Welcome to the Advanced Server Rendering guide, where you will learn all about using React Query with streaming, Server Components and the Next.js app router.
You might want to read the Server Rendering & Hydration guide before this one as it teaches the basics for using React Query with SSR, and Performance & Request Waterfalls as well as Prefetching & Router Integration also contains valuable background.
Before we start, let's note that while the initialData
approach outlined in the SSR guide also works with Server Components, we'll focus this guide on the hydration APIs.
Server Components & Next.js app router #
We won't cover Server Components in depth here, but the short version is that they are components that are guaranteed to only run on the server, both for the initial page view and also on page transitions. This is similar to how Next.js getServerSideProps
/getStaticProps
and Remix loader
works, as these also always run on the server but while those can only return data, Server Components can do a lot more. The data part is central to React Query however, so let's focus on that.
How do we take what we learned in the Server Rendering guide about passing data prefetched in framework loaders to the app and apply that to Server Components and the Next.js app router? The best way to start thinking about this is to consider Server Components as "just" another framework loader.
A quick note on terminology #
So far in these guides, we've been talking about the server and the client. It's important to note that confusingly enough this does not match 1-1 with Server Components and Client Components. Server Components are guaranteed to only run on the server, but Client Components can actually run in both places. The reason for this is that they can also render during the initial server rendering pass.
One way to think of this is that even though Server Components also render, they happen during a "loader phase" (always happens on the server), while Client Components run during the "application phase". That application can run both on the server during SSR, and in for example a browser. Where exactly that application runs and if it runs during SSR or not might differ between frameworks.
Initial setup #
The first step of any React Query setup is always to create a queryClient
and wrap your application in a QueryClientProvider
. With Server Components, this looks mostly the same across frameworks, one difference being the filename conventions:
1// In Next.js, this file would be called: app/providers.jsx
2'use client'
3
4// Since QueryClientProvider relies on useContext under the hood, we have to put 'use client' on top
5import {
6 isServer,
7 QueryClient,
8 QueryClientProvider,
9} from '@tanstack/react-query'
10
11function 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
23let browserQueryClient: QueryClient | undefined = undefined
24
25function 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
39export default function Providers({ children }) {
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}
1// In Next.js, this file would be called: app/layout.jsx
2import Providers from './providers'
3
4export default function RootLayout({ children }) {
5 return (
6 <html lang="en">
7 <head />
8 <body>
9 <Providers>{children}</Providers>
10 </body>
11 </html>
12 )
13}
This part is pretty similar to what we did in the SSR guide, we just need to split things up into two different files.
Prefetching and de/hydrating data #
Let's next look at how to actually prefetch data and dehydrate and hydrate it. This is what it looked like using the Next.js pages router:
1// pages/posts.jsx
2import {
3 dehydrate,
4 HydrationBoundary,
5 QueryClient,
6 useQuery,
7} from '@tanstack/react-query'
8
9// This could also be getServerSideProps
10export async function getStaticProps() {
11 const queryClient = new QueryClient()
12
13 await queryClient.prefetchQuery({
14 queryKey: ['posts'],
15 queryFn: getPosts,
16 })
17
18 return {
19 props: {
20 dehydratedState: dehydrate(queryClient),
21 },
22 }
23}
24
25function Posts() {
26 // This useQuery could just as well happen in some deeper child to
27 // the <PostsRoute>, data will be available immediately either way
28 //
29 // Note that we are using useQuery here instead of useSuspenseQuery.
30 // Because this data has already been prefetched, there is no need to
31 // ever suspend in the component itself. If we forget or remove the
32 // prefetch, this will instead fetch the data on the client, while
33 // using useSuspenseQuery would have had worse side effects.
34 const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts })
35
36 // This query was not prefetched on the server and will not start
37 // fetching until on the client, both patterns are fine to mix
38 const { data: commentsData } = useQuery({
39 queryKey: ['posts-comments'],
40 queryFn: getComments,
41 })
42
43 // ...
44}
45
46export default function PostsRoute({ dehydratedState }) {
47 return (
48 <HydrationBoundary state={dehydratedState}>
49 <Posts />
50 </HydrationBoundary>
51 )
52}
Converting this to the app router actually looks pretty similar, we just need to move things around a bit. First, we'll create a Server Component to do the prefetching part:
1// app/posts/page.jsx
2import {
3 dehydrate,
4 HydrationBoundary,
5 QueryClient,
6} from '@tanstack/react-query'
7import Posts from './posts'
8
9export default async function PostsPage() {
10 const queryClient = new QueryClient()
11
12 await queryClient.prefetchQuery({
13 queryKey: ['posts'],
14 queryFn: getPosts,
15 })
16
17 return (
18 // Neat! Serialization is now as easy as passing props.
19 // HydrationBoundary is a Client Component, so hydration will happen there.
20 <HydrationBoundary state={dehydrate(queryClient)}>
21 <Posts />
22 </HydrationBoundary>
23 )
24}
Next, we'll look at what the Client Component part looks like:
1// app/posts/posts.jsx
2'use client'
3
4export default function Posts() {
5 // This useQuery could just as well happen in some deeper
6 // child to <Posts>, data will be available immediately either way
7 const { data } = useQuery({
8 queryKey: ['posts'],
9 queryFn: () => getPosts(),
10 })
11
12 // This query was not prefetched on the server and will not start
13 // fetching until on the client, both patterns are fine to mix.
14 const { data: commentsData } = useQuery({
15 queryKey: ['posts-comments'],
16 queryFn: getComments,
17 })
18
19 // ...
20}
One neat thing about the examples above is that the only thing that is Next.js-specific here are the file names, everything else would look the same in any other framework that supports Server Components.
In the SSR guide, we noted that you could get rid of the boilerplate of having <HydrationBoundary>
in every route. This is not possible with Server Components.
NOTE: If you encounter a type error while using async Server Components with TypeScript versions lower than
5.1.3
and@types/react
versions lower than18.2.8
, it is recommended to update to the latest versions of both. Alternatively, you can use the temporary workaround of adding{/* @ts-expect-error Server Component */}
when calling this component inside another. For more information, see Async Server Component TypeScript Error in the Next.js 13 docs.
NOTE: If you encounter an error
Only plain objects, and a few built-ins, can be passed to Server Actions. Classes or null prototypes are not supported.
make sure that you're not passing to queryFn a function reference, instead call the function because queryFn args has a bunch of properties and not all of it would be serializable. see Server Action only works when queryFn isn't a reference.
Nesting Server Components #
A nice thing about Server Components is that they can be nested and exist on many levels in the React tree, making it possible to prefetch data closer to where it's actually used instead of only at the top of the application (just like Remix loaders). This can be as simple as a Server Component rendering another Server Component (we'll leave the Client Components out in this example for brevity):
1// app/posts/page.jsx
2import {
3 dehydrate,
4 HydrationBoundary,
5 QueryClient,
6} from '@tanstack/react-query'
7import Posts from './posts'
8import CommentsServerComponent from './comments-server'
9
10export default async function PostsPage() {
11 const queryClient = new QueryClient()
12
13 await queryClient.prefetchQuery({
14 queryKey: ['posts'],
15 queryFn: getPosts,
16 })
17
18 return (
19 <HydrationBoundary state={dehydrate(queryClient)}>
20 <Posts />
21 <CommentsServerComponent />
22 </HydrationBoundary>
23 )
24}
25
26// app/posts/comments-server.jsx
27import {
28 dehydrate,
29 HydrationBoundary,
30 QueryClient,
31} from '@tanstack/react-query'
32import Comments from './comments'
33
34export default async function CommentsServerComponent() {
35 const queryClient = new QueryClient()
36
37 await queryClient.prefetchQuery({
38 queryKey: ['posts-comments'],
39 queryFn: getComments,
40 })
41
42 return (
43 <HydrationBoundary state={dehydrate(queryClient)}>
44 <Comments />
45 </HydrationBoundary>
46 )
47}
As you can see, it's perfectly fine to use <HydrationBoundary>
in multiple places, and create and dehydrate multiple queryClient
for prefetching.
Note that because we are awaiting getPosts
before rendering CommentsServerComponent
this would lead to a server side waterfall:
1. |> getPosts()
2. |> getComments()
If the server latency to the data is low, this might not be a huge issue, but is still worth pointing out.
In Next.js, besides prefetching data in page.tsx
, you can also do it in layout.tsx
, and in parallel routes. Because these are all part of the routing, Next.js knows how to fetch them all in parallel. So if CommentsServerComponent
above was instead expressed as a parallel route, the waterfall would be flattened automatically.
As more frameworks start supporting Server Components, they might have other routing conventions. Read your framework docs for details.
Alternative: Use a single queryClient
for prefetching #
In the example above, we create a new queryClient
for each Server Component that fetches data. This is the recommended approach, but if you want to, you can alternatively create a single one that is reused across all Server Components:
1// app/getQueryClient.jsx
2import { QueryClient } from '@tanstack/react-query'
3import { cache } from 'react'
4
5// cache() is scoped per request, so we don't leak data between requests
6const getQueryClient = cache(() => new QueryClient())
7export default getQueryClient
The benefit of this is that you can call getQueryClient()
to get a hold of this client anywhere that gets called from a Server Component, including utility functions. The downside is that every time you call dehydrate(getQueryClient())
, you serialize the entire queryClient
, including queries that have already been serialized before and are unrelated to the current Server Component which is unnecessary overhead.
Next.js already dedupes requests that utilize fetch()
, but if you are using something else in your queryFn
, or if you use a framework that does not dedupe these requests automatically, using a single queryClient
as described above might make sense, despite the duplicated serialization.
As a future improvement, we might look into creating a
dehydrateNew()
function (name pending) that only dehydrate queries that are new since the last call todehydrateNew()
. Feel free to get in touch if this sounds interesting and like something you want to help out with!
Data ownership and revalidation #
With Server Components, it's important to think about data ownership and revalidation. To explain why, let's look at a modified example from above:
1// app/posts/page.jsx
2import {
3 dehydrate,
4 HydrationBoundary,
5 QueryClient,
6} from '@tanstack/react-query'
7import Posts from './posts'
8
9export default async function PostsPage() {
10 const queryClient = new QueryClient()
11
12 // Note we are now using fetchQuery()
13 const posts = await queryClient.fetchQuery({
14 queryKey: ['posts'],
15 queryFn: getPosts,
16 })
17
18 return (
19 <HydrationBoundary state={dehydrate(queryClient)}>
20 {/* This is the new part */}
21 <div>Nr of posts: {posts.length}</div>
22 <Posts />
23 </HydrationBoundary>
24 )
25}
We are now rendering data from the getPosts
query both in a Server Component and in a Client Component. This will be fine for the initial page render, but what happens when the query revalidates on the client for some reason when staleTime
has been passed?
React Query has no idea of how to revalidate the Server Component, so if it refetches the data on the client, causing React to rerender the list of posts, the Nr of posts: {posts.length}
will end up out of sync.
This is fine if you set staleTime: Infinity
, so that React Query never revalidates, but this is probably not what you want if you are using React Query in the first place.
Using React Query with Server Components makes most sense if:
- You have an app using React Query and want to migrate to Server Components without rewriting all the data fetching
- You want a familiar programming paradigm, but want to still sprinkle in the benefits of Server Components where it makes most sense
- You have some use case that React Query covers, but that your framework of choice does not cover
It's hard to give general advice on when it makes sense to pair React Query with Server Components and not. If you are just starting out with a new Server Components app, we suggest you start out with any tools for data fetching your framework provides you with and avoid bringing in React Query until you actually need it. This might be never, and that's fine, use the right tool for the job!
If you do use it, a good rule of thumb is to avoid queryClient.fetchQuery
unless you need to catch errors. If you do use it, don't render its result on the server or pass the result to another component, even a Client Component one.
From the React Query perspective, treat Server Components as a place to prefetch data, nothing more.
Of course, it's fine to have Server Components own some data, and Client Components own other, just make sure those two realities don't get out of sync.
Streaming with Server Components #
The Next.js app router automatically streams any part of the application that is ready to be displayed to the browser as soon as possible, so finished content can be displayed immediately without waiting for still pending content. It does this along <Suspense>
boundary lines. Note that if you create a file loading.tsx
, this automatically creates a <Suspense>
boundary behind the scenes.
With the prefetching patterns described above, React Query is perfectly compatible with this form of streaming. As the data for each Suspense boundary resolves, Next.js can render and stream the finished content to the browser. This works even if you are using useQuery
as outlined above because the suspending actually happens when you await
the prefetch.
As of React Query v5.40.0, you don't have to await
all prefetches for this to work, as pending
Queries can also be dehydrated and sent to the client. This lets you kick off prefetches as early as possible without letting them block an entire Suspense boundary, and streams the data to the client as the query finishes. This can be useful for example if you want to prefetch some content that is only visible after some user interaction, or say if you want to await
and render the first page of an infinite query, but start prefetching page 2 without blocking rendering.
To make this work, we have to instruct the queryClient
to also dehydrate
pending Queries. We can do this globally, or by passing that option directly to hydrate
.
We will also need to move the getQueryClient()
function out of our app/providers.jsx
file as we want to use it in our server component and our client provider.
1// app/get-query-client.ts
2import {
3 isServer,
4 QueryClient,
5 defaultShouldDehydrateQuery,
6} from '@tanstack/react-query'
7
8function makeQueryClient() {
9 return new QueryClient({
10 defaultOptions: {
11 queries: {
12 staleTime: 60 * 1000,
13 },
14 dehydrate: {
15 // include pending queries in dehydration
16 shouldDehydrateQuery: (query) =>
17 defaultShouldDehydrateQuery(query) ||
18 query.state.status === 'pending',
19 },
20 },
21 })
22}
23
24let browserQueryClient: QueryClient | undefined = undefined
25
26export function getQueryClient() {
27 if (isServer) {
28 // Server: always make a new query client
29 return makeQueryClient()
30 } else {
31 // Browser: make a new query client if we don't already have one
32 // This is very important, so we don't re-make a new client if React
33 // suspends during the initial render. This may not be needed if we
34 // have a suspense boundary BELOW the creation of the query client
35 if (!browserQueryClient) browserQueryClient = makeQueryClient()
36 return browserQueryClient
37 }
38}
Note: This works in NextJs and Server Components because React can serialize Promises over the wire when you pass them down to Client Components.
Then, all we need to do is provide a HydrationBoundary
, but we don't need to await
prefetches anymore:
1// app/posts/page.jsx
2import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
3import { getQueryClient } from './get-query-client'
4import Posts from './posts'
5
6// the function doesn't need to be `async` because we don't `await` anything
7export default function PostsPage() {
8 const queryClient = getQueryClient()
9
10 // look ma, no await
11 queryClient.prefetchQuery({
12 queryKey: ['posts'],
13 queryFn: getPosts,
14 })
15
16 return (
17 <HydrationBoundary state={dehydrate(queryClient)}>
18 <Posts />
19 </HydrationBoundary>
20 )
21}
On the client, the Promise will be put into the QueryCache for us. That means we can now call useSuspenseQuery
inside the Posts
component to "use" that Promise (which was created on the Server):
1// app/posts/posts.tsx
2'use client'
3
4export default function Posts() {
5 const { data } = useSuspenseQuery({ queryKey: ['posts'], queryFn: getPosts })
6
7 // ...
8}
Note that you could also
useQuery
instead ofuseSuspenseQuery
, and the Promise would still be picked up correctly. However, NextJs won't suspend in that case and the component will render in thepending
status, which also opts out of server rendering the content.
If you're using non-JSON data types and serialize the query results on the server, you can specify the dehydrate.serializeData
and hydrate.deserializeData
options to serialize and deserialize the data on each side of the boundary to ensure the data in the cache is the same format both on the server and the client:
1// app/get-query-client.ts
2import { QueryClient, defaultShouldDehydrateQuery } from '@tanstack/react-query'
3import { deserialize, serialize } from './transformer'
4
5function makeQueryClient() {
6 return new QueryClient({
7 defaultOptions: {
8 // ...
9 hydrate: {
10 deserializeData: deserialize,
11 },
12 dehydrate: {
13 serializeData: serialize,
14 },
15 },
16 })
17}
18
19// ...
1// app/posts/page.tsx
2import {
3 dehydrate,
4 HydrationBoundary,
5 QueryClient,
6} from '@tanstack/react-query'
7import { getQueryClient } from './get-query-client'
8import { serialize } from './transformer'
9import Posts from './posts'
10
11export default function PostsPage() {
12 const queryClient = getQueryClient()
13
14 // look ma, no await
15 queryClient.prefetchQuery({
16 queryKey: ['posts'],
17 queryFn: () => getPosts().then(serialize), // <-- serialize the data on the server
18 })
19
20 return (
21 <HydrationBoundary state={dehydrate(queryClient)}>
22 <Posts />
23 </HydrationBoundary>
24 )
25}
1// app/posts/posts.tsx
2'use client'
3
4export default function Posts() {
5 const { data } = useSuspenseQuery({ queryKey: ['posts'], queryFn: getPosts })
6
7 // ...
8}
Now, your getPosts
function can return e.g. Temporal
datetime objects and the data will be serialized and deserialized on the client, assuming your transformer can serialize and deserialize those data types.
For more information, check out the Next.js App with Prefetching Example.
Experimental streaming without prefetching in Next.js #
While we recommend the prefetching solution detailed above because it flattens request waterfalls both on the initial page load and any subsequent page navigation, there is an experimental way to skip prefetching altogether and still have streaming SSR work: @tanstack/react-query-next-experimental
This package will allow you to fetch data on the server (in a Client Component) by just calling useSuspenseQuery
in your component. Results will then be streamed from the server to the client as SuspenseBoundaries resolve. If you call useSuspenseQuery
without wrapping it in a <Suspense>
boundary, the HTML response won't start until the fetch resolves. This can be when you want depending on the situation, but keep in mind that this will hurt your TTFB.
To achieve this, wrap your app in the ReactQueryStreamedHydration
component:
1// app/providers.tsx
2'use client'
3
4import {
5 isServer,
6 QueryClient,
7 QueryClientProvider,
8} from '@tanstack/react-query'
9import * as React from 'react'
10import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental'
11
12function makeQueryClient() {
13 return new QueryClient({
14 defaultOptions: {
15 queries: {
16 // With SSR, we usually want to set some default staleTime
17 // above 0 to avoid refetching immediately on the client
18 staleTime: 60 * 1000,
19 },
20 },
21 })
22}
23
24let browserQueryClient: QueryClient | undefined = undefined
25
26function getQueryClient() {
27 if (isServer) {
28 // Server: always make a new query client
29 return makeQueryClient()
30 } else {
31 // Browser: make a new query client if we don't already have one
32 // This is very important, so we don't re-make a new client if React
33 // suspends during the initial render. This may not be needed if we
34 // have a suspense boundary BELOW the creation of the query client
35 if (!browserQueryClient) browserQueryClient = makeQueryClient()
36 return browserQueryClient
37 }
38}
39
40export function Providers(props: { children: React.ReactNode }) {
41 // NOTE: Avoid useState when initializing the query client if you don't
42 // have a suspense boundary between this and the code that may
43 // suspend because React will throw away the client on the initial
44 // render if it suspends and there is no boundary
45 const queryClient = getQueryClient()
46
47 return (
48 <QueryClientProvider client={queryClient}>
49 <ReactQueryStreamedHydration>
50 {props.children}
51 </ReactQueryStreamedHydration>
52 </QueryClientProvider>
53 )
54}
For more information, check out the NextJs Suspense Streaming Example.
The big upside is that you no longer need to prefetch queries manually to have SSR work, and it even still streams in the result! This gives you phenomenal DX and lower code complexity.
The downside is easiest to explain if we look back at the complex request waterfall example in the Performance & Request Waterfalls guide. Server Components with prefetching effectively eliminates the request waterfalls both for the initial page load and any subsequent navigation. This prefetch-less approach however will only flatten the waterfalls on the initial page load but ends up the same deep waterfall as the original example on page navigations:
1. |> JS for <Feed>
2. |> getFeed()
3. |> JS for <GraphFeedItem>
4. |> getGraphDataById()
This is even worse than with getServerSideProps
/getStaticProps
, since with those we could at least parallelize data- and code-fetching.
If you value DX/iteration/shipping speed with low code complexity over performance, don't have deeply nested queries, or are on top of your request waterfalls with parallel fetching using tools like useSuspenseQueries
, this can be a good tradeoff.
It might be possible to combine the two approaches, but even we haven't tried that out yet. If you do try this, please report back your findings, or even update these docs with some tips!
Final words #
Server Components and streaming are still fairly new concepts and we are still figuring out how React Query fits in and what improvements we can make to the API. We welcome suggestions, feedback and bug reports!
Similarly, it would be impossible to teach all the intricacies of this new paradigm all in one guide, on the first try. If you are missing some piece of information here or have suggestions on how to improve this content, also get in touch, or even better, click the "Edit on GitHub" button below and help us out.