|
Initially, I was tempted to title this discussion “TanStack Query’s SSR data-fetching model was not made to work with Next.js.” But that felt a little harsh without a proper discussion or without fully knowing if I am missing something or not. There seems to be a mismatch between React Query’s recommended SSR model for Next.js and the granular, component-owned data-fetching model encouraged by the Next.js App Router. Unlike some other frameworks, Next.js really discourages you to fetch all of the data you need for a given page. It seems that the framework would much rather have you fetch all of the data that each subtree or component needs for itself, instead of having all the data fetching be something owned by the route itself. Next.js encourages fetching data in the component or subtree that consumes it. Identical Consider an order-details page with a static layout and several independently loading regions: <PageWithHeaderLayout
header={
<Suspense fallback={<OrderHeaderSkeleton />}>
<OrderHeader />
</Suspense>
}
>
<Suspense fallback={<FulfillmentSkeleton />}>
<OrderFulfillment />
</Suspense>
<Suspense fallback={<ProductionHistorySkeleton />}>
<ProductionHistory />
</Suspense>
<Suspense fallback={<OrderInformationSkeleton />}>
<OrderInformation />
</Suspense>
</PageWithHeaderLayout>Each dynamic component needs the same order object: function OrderHeader() {
const { data } = useSuspenseQuery(orderDetailsQueryOptions(orderId));
return /* ... */;
}The desired behavior is for the first subscriber to start the query during server rendering.
The prefetch modelThe currently recommended React Query SSR pattern is for a Server Component to prefetch the query and render a export default function Page() {
const queryClient = getQueryClient();
queryClient.prefetchQuery(serverOrderDetailsQueryOptions(orderId));
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<OrderDetailsPage />
</HydrationBoundary>
);
}This works, including streaming pending queries in recent versions, but route-level prefetching (higher order RSC like described above) has an important consequence in Next.js: the request-time data access happens in a single RSC parent, outside the granular Suspense boundaries that describe the page’s loading behavior. That defeats the purpose of structuring the page like this: <StaticPageLayout>
<Suspense fallback={<HeaderSkeleton />}>
<DynamicHeader />
</Suspense>
<Suspense fallback={<ReportSkeleton />}>
<DynamicReport />
</Suspense>
</StaticPageLayout>The Suspense boundaries suggest that the static layout and each dynamic region can be rendered independently. However, a route-level prefetch moves the actual request-time dependency above all those boundaries. You could then say, (and this is the best I could come up with so far) that we could move the prefetching down into each subtree: async function OrderHeaderBoundary() {
const queryClient = getQueryClient();
await queryClient.prefetchQuery(
serverOrderDetailsQueryOptions(orderId),
);
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<OrderHeader />
</HydrationBoundary>
);
}The page could then render it under Suspense: <Suspense fallback={<OrderHeaderSkeleton />}>
<OrderHeaderBoundary />
</Suspense>This preserves the granularity expected by Next.js, but it feels like the wrong abstraction. This is where the two models appear incompatible:
Neither option produces the component-owned model shown in the original example, where a component calls |
Replies: 2 comments 5 replies
|
The model you want is the experimental streamed hydration path, not the route-level Wrap the app in <QueryClientProvider client={queryClient}>
<ReactQueryStreamedHydration>{children}</ReactQueryStreamedHydration>
</QueryClientProvider>The important boundary is the query function: because it lives in a client component module, don't import DB/server-only code there. If the query needs a server-only procedure, expose that through a route handler/server function/RPC endpoint, or keep a small Server Component prefetch wrapper at the subtree boundary. |
I’m not really sure that this is the case. The way I see it, calling The suspension happens where If that’s not the case, please show a reproduction.
I am the first one to state / admit that React Query was not specifically made to work with nextJs. It’s a client-side async state manager that predates the app router by years. If you fully embrace the nextJs model, including cache components, why would you need React Query? |
I would not use the
StreamedHydrationpackage. It builds on a nextJs specific API and we haven’t really worked in that direction lately. It’s also quite magic. Moving prefetching down is an idea, TanStack Start is an idea, or not using Query is also an idea.