Tanstack Query prefetching with Nextjs cache components, dynamic searchParams, and separate hydration boundaries #95933
Replies: 1 comment
-
|
Great, well-structured question. The most important thing I'd flag is a subtle server/client query-key mismatch — everything else follows from that. The key-parity bug (your Q8): your server and client build the query key differently, so they can silently diverge. Server-side the key is the resolved searchParams, where repeated params are arrays. Client-side you build it with: ts Object.fromEntries collapses repeated keys to the last value. You can confirm it in a console: js So ?tag=a&tag=b becomes { tag: "b" } on the client but { tag: ["a", "b"] } on the server. TanStack hashes keys deterministically, so those two don't match → the hydrated entry is ignored and the client refetches, defeating the prefetch. If any of your params can repeat, normalize both sides to the same shape before building the key (e.g. always coerce to arrays, or use getAll explicitly). On "use cache" placement (Q3–Q5): I'd move the cache boundary down to the pure data function rather than wrapping the whole prefetch/dehydrate. dehydrate output includes TanStack metadata such as dataUpdatedAt; caching the whole dehydrated state freezes those timestamps at cache-write time. With staleTime: Infinity you won't notice immediately, but it becomes a subtle staleness bug the moment you lower staleTime. Caching only listItems keeps the cached unit purely serializable data and lets the QueryClient/dehydration run fresh per request. Passing one Promise to multiple HydrationBoundary (Q2, Q6): safe. The promise resolves once and all awaiters share the value; hydrate is idempotent per query key, so hydrating the same key through two boundaries won't duplicate or overwrite. Reuse across separate boundaries (Q8): yes — both components read from the same singleton client and key, so they share one cache entry once the key parity above is fixed. Simpler pattern (Q7): since both consumers share one query, you don't strictly need two HydrationBoundary — one boundary wrapping both children is enough. Separate boundaries only pay off when each streams a different dehydrated state. Resolving searchParams (Q1): chaining off the promise to keep the page component synchronous is a reasonable way to preserve the static shell and streaming; just keep the object you feed into the key stable and serializable. Since cacheComponents is still preview, the caching specifics may shift between canaries — but the key-parity issue is version-independent and worth fixing regardless. Hope this helps — let me know if normalizing the key resolves the missed hydration on your end. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Summary
I’m trying to use TanStack Query prefetching with Next.js Cache Components while keeping different parts of the page under separate
<Suspense>and<HydrationBoundary>components.The query key depends on
searchParams, so I resolve thesearchParamspromise before passing the resulting plain object into a cached prefetch function.Page implementation
ItemsCountItemsListuseItemsgetQueryClientimplementationEnvironment
Package versions
{ "next": "16.3.0-preview.6", "@tanstack/react-query": "^5.101.2" }next.config.tsGoals
<Suspense>.connection().Questions
searchParamslike this the intended approach?Is it safe and supported to pass the same
Promise<DehydratedState>to multiple<HydrationBoundary>components?Is
"use cache"appropriate around the entire TanStack Query prefetch and dehydration function?Would it be better to put
"use cache"only insidelistItems()and create theQueryClientand dehydrated state outside the cached function?Could caching the dehydrated state cause issues because it contains query timestamps or other TanStack Query metadata?
Can hydrating the same query through multiple boundaries cause duplicated state, overwrites, or unexpected client-side behavior?
Is there a more idiomatic pattern for this case where the query key depends on asynchronous
searchParams, but the page should still preserve streaming?Will both
ItemsCountandItemsListcorrectly reuse the same hydrated query even though they are rendered under separate hydration boundaries?I also opened the same discussion in Tanstack for additional feedback: TanStack/query#11083
Additional information
No response
Example
No response
Beta Was this translation helpful? Give feedback.
All reactions