Skip to content

Commit f2659f4

Browse files
committed
TanStack/Query hydration in Next.js App Router
1 parent c5cd4de commit f2659f4

File tree

6 files changed

+449
-183
lines changed

6 files changed

+449
-183
lines changed
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
import Providers from './providers'
2+
13
export default function RootLayout({
24
children,
35
}: Readonly<{
46
children: React.ReactNode
57
}>) {
68
return (
79
<html lang="en">
8-
<body>{children}</body>
10+
<body>
11+
<Providers>{children}</Providers>
12+
</body>
913
</html>
1014
)
1115
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
'use client'
2+
3+
import { useQuery } from '@tanstack/react-query'
4+
import { getPost } from '../page'
5+
import Link from 'next/link'
6+
7+
export const Post = ({ postId }: { postId: string }) => {
8+
const { data, isPending, isError } = useQuery({
9+
queryKey: ['posts', postId],
10+
queryFn: ({ queryKey: [, postId] }) => getPost(postId as string),
11+
})
12+
if (isPending) return <div>Loading...</div>
13+
if (isError) return <div>Error</div>
14+
return (
15+
<div>
16+
<Link href="/posts">Back to posts</Link>
17+
<div>ID: {data?.id}</div>
18+
<h1>Title: {data?.title}</h1>
19+
<div>Body: {data?.body}</div>
20+
21+
<div>
22+
<Link href={`/posts/${Number(postId) + 1}`}>Next post</Link>
23+
</div>
24+
</div>
25+
)
26+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// app/posts/[postId]/page.tsx
2+
import {
3+
dehydrate,
4+
HydrationBoundary,
5+
QueryClient,
6+
} from '@tanstack/react-query'
7+
import { Post } from './_components/post'
8+
9+
export async function getPost(postId: string) {
10+
console.debug('getPost called with postId:', postId)
11+
const res = await fetch(
12+
`https://jsonplaceholder.typicode.com/posts/${postId}`
13+
)
14+
return res.json()
15+
}
16+
17+
export default async function PostPage({
18+
params,
19+
}: {
20+
params: Promise<{ postId: string }>
21+
}) {
22+
const { postId } = await params
23+
const queryClient = new QueryClient()
24+
25+
await queryClient.prefetchQuery({
26+
queryKey: ['posts', postId],
27+
queryFn: ({ queryKey: [, postId] }) => getPost(postId as string),
28+
})
29+
30+
return (
31+
// Neat! Serialization is now as easy as passing props.
32+
// HydrationBoundary is a Client Component, so hydration will happen there.
33+
<HydrationBoundary state={dehydrate(queryClient)}>
34+
<Post postId={postId} />
35+
</HydrationBoundary>
36+
)
37+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import Link from 'next/link'
2+
3+
export default function PostsPage() {
4+
return (
5+
<div>
6+
Posts
7+
<Link href="/posts/1">Post 1</Link>
8+
<Link href="/posts/2">Post 2</Link>
9+
<Link href="/posts/3">Post 3</Link>
10+
<Link href="/posts/4">Post 4</Link>
11+
<Link href="/posts/5">Post 5</Link>
12+
<Link href="/posts/6">Post 6</Link>
13+
<Link href="/posts/7">Post 7</Link>
14+
<Link href="/posts/8">Post 8</Link>
15+
<Link href="/posts/9">Post 9</Link>
16+
<Link href="/posts/10">Post 10</Link>
17+
</div>
18+
)
19+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// In Next.js, this file would be called: app/providers.tsx
2+
'use client'
3+
4+
// Since QueryClientProvider relies on useContext under the hood, we have to put 'use client' on top
5+
import {
6+
isServer,
7+
QueryClient,
8+
QueryClientProvider,
9+
} from '@tanstack/react-query'
10+
11+
function makeQueryClient() {
12+
return new QueryClient({
13+
defaultOptions: {
14+
queries: {
15+
// With SSR, we usually want to set some default staleTime
16+
// above 0 to avoid refetching immediately on the client
17+
staleTime: 60 * 1000,
18+
},
19+
},
20+
})
21+
}
22+
23+
let browserQueryClient: QueryClient | undefined = undefined
24+
25+
function getQueryClient() {
26+
if (isServer) {
27+
// Server: always make a new query client
28+
return makeQueryClient()
29+
} else {
30+
// Browser: make a new query client if we don't already have one
31+
// This is very important, so we don't re-make a new client if React
32+
// suspends during the initial render. This may not be needed if we
33+
// have a suspense boundary BELOW the creation of the query client
34+
if (!browserQueryClient) browserQueryClient = makeQueryClient()
35+
return browserQueryClient
36+
}
37+
}
38+
39+
export default function Providers({ children }: { children: React.ReactNode }) {
40+
// NOTE: Avoid useState when initializing the query client if you don't
41+
// have a suspense boundary between this and the code that may
42+
// suspend because React will throw away the client on the initial
43+
// render if it suspends and there is no boundary
44+
const queryClient = getQueryClient()
45+
46+
return (
47+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
48+
)
49+
}

0 commit comments

Comments
 (0)