new·The score now tells you which way it movedA brain's exam only ever grows: its own material writes questions, and so does every question a real caller asked and did not get answered. The score is a percentage over that growing set, so a brain that learned more could post a smaller number — and this week three did. One of them answered two MORE questions than the week before and showed eighteen points less. Printed as a single percentage, that reads as decline to a reader and as punishment to anyone who contributes material.all news →
mozg.beta
Sign in

TanStack Query · React · all subjects

ssr & hydration

39 notes, read out of this brain and free to use. Each one was extracted from a source and is re-checked against its exam.

QueryClient must be created inside the app on each request

When doing server rendering, the queryClient instance must be created inside the app (in React state or an instance ref) rather than at the file root level. This ensures that data is not shared between different users and requests, while still only creating the queryClient once per component lifecycle. Creating the queryClient at the root level makes the cache shared between all requests and means all data gets passed to all users, which leaks sensitive data.

Default staleTime for SSR should be above zero

With SSR, it is recommended to set a default staleTime above 0 (for example, 60 * 1000 milliseconds) to avoid refetching immediately on the client. This prevents unnecessary double fetching after the initial server render.

Three steps of server rendering with React Query: prefetch, dehydrate, hydrate

On the server, data must be prefetched before generating the markup. That data must then be dehydrated into a serializable format to embed in the markup. On the client, the data must be hydrated into a React Query cache to avoid doing a new fetch.

useSuspenseQuery in SSR requires always prefetching all queries

While it is possible to replace useQuery with useSuspenseQuery in SSR contexts to get Suspense for loading states on the client, you must always prefetch all your queries. If you forget to prefetch a query when using useSuspenseQuery, the data will Suspend and get fetched on the server but never be hydrated to the client, causing a markup hydration mismatch.

initialData option as quick SSR alternative

A quick way to use initial data without dehydration/hydration is to pass raw data as the initialData option to useQuery. This requires minimal setup but has tradeoffs: initialData must be passed down through the component tree, dataUpdatedAt is based on page load time rather than server fetch time, and initialData will never overwrite existing cache data even if fresher.

HydrationBoundary wraps the component tree with dehydrated state

After dehydrating the queryClient, wrap the relevant part of the component tree with HydrationBoundary component passing the dehydrated state. This can be done for each route or at the top of the application to avoid boilerplate. The dehydratedState comes from the framework loader.

Three queryClients involved in server rendering

In server rendering, there are actually three queryClient instances involved. The framework loader (preloading phase before rendering) has its own queryClient for prefetching. The dehydrated result is passed to both the server rendering process and the client rendering process, each with their own queryClient. This ensures both start with the same data and return the same markup.

Prefetch queries in parallel using Promise.all

When prefetching multiple queries in the framework loader function, use await Promise.all(...) to fetch the queries in parallel when possible. This avoids unnecessary request waterfalls on the server.

Queries can be prefetched or fetched client-only

It is fine to mix prefetched and non-prefetched queries. Queries that aren't prefetched won't be server rendered; instead they will be fetched on the client after the application is interactive. This can be useful for content shown only after user interaction or far down the page to avoid blocking more critical content.

prefetchQuery never throws errors; use fetchQuery for critical content error handling

queryClient.prefetchQuery(...) never throws errors and dehydrate(...) only includes successful queries, not failed ones. This means failed queries are retried on the client and server-rendered output includes loading states instead of full content. For critical content where you need error handling, use queryClient.fetchQuery(...) instead, which will throw errors when it fails.

shouldDehydrateQuery option to include failed queries

By default, dehydrate(queryClient) only includes successful queries. If you want to include failed queries in the dehydrated state to avoid retries, use the shouldDehydrateQuery option to override the default function and implement your own logic.

Serialization safety: use Serialize JavaScript or devalue for custom SSR

When embedding dehydrated state into markup in custom SSR setups, do not use JSON.stringify directly as it does not escape XSS vectors like <script>alert('Oh no..')</script>. Use libraries like Serialize JavaScript or devalue which are safe against XSS injections out of the box. Note that superjson also does not escape values and is unsafe for custom SSR without an extra escaping step.

Unsupported types in framework serialization

Next.js and Remix only support returning safely serializable/parsable values, and therefore do not support undefined, Error, Date, Map, Set, BigInt, Infinity, NaN, -0, or regular expressions. Queries cannot return these values. Use packages like superjson to handle custom types.

Staleness measured from server fetch time with UTC

A query is considered stale depending on when it was dataUpdatedAt. The server needs the correct UTC time for this to work properly. Since staleTime defaults to 0, queries will be refetched in the background on page load by default. Use a higher staleTime to avoid this double fetching, especially if not caching markup.

gcTime defaults to Infinity on server and clears after request

On the server, gcTime defaults to Infinity which disables manual garbage collection and automatically clears memory once a request finishes. If explicitly setting a non-Infinity gcTime, you are responsible for clearing the cache early. Avoid setting gcTime to 0 as it may result in hydration errors. If a shorter gcTime is needed, recommend setting it to at least 2 * 1000 milliseconds.

Clear queryClient cache after dehydration to reduce server memory

To clear the cache after it is not needed and lower memory consumption on the server, add a call to queryClient.clear() after the request is handled and dehydrated state has been sent to the client.

High server memory consumption from creating QueryClient per request

Creating a QueryClient for every request on the server creates an isolated cache that is preserved in memory for the gcTime period. This can lead to high memory consumption in case of a high number of requests. Configure gcTime appropriately or clear the cache with queryClient.clear() after each request.

Prefetching dependent queries in server loaders

To prefetch dependent queries in framework loaders, first use queryClient.fetchQuery to fetch the first query and get its result. Then conditionally prefetch dependent queries based on the result. Use await Promise.all if prefetching multiple independent queries in parallel.

Next.js rewrites cause second hydration without referential equality

Using Next.js rewrites together with Automatic Static Optimization or getStaticProps causes a second hydration by React Query because Next.js must parse rewrites on the client and collect params for router.query. This results in missing referential equality for hydration data, triggering re-renders where data is used as component props or in useEffect/useMemo dependency arrays.

Server rendering flattens request waterfalls for initial page load

With server rendering, the request waterfall is reduced from three roundtrips (Markup without content, JS, Query) to two (Markup with content and initial data, JS). On the server, dependent queries still require a waterfall, but modern frameworks with prefetching can fetch initial code and data in parallel during navigation.

SPA navigation does not get server rendering benefits without prefetching

Server rendering benefits only apply to the initial page load. For subsequent navigation in SPAs, without prefetching patterns, you get client-side request waterfalls again. Using prefetching patterns in frameworks like Next.js or Remix for client-side navigation can fetch initial code and data in parallel, improving the waterfall.

Dehydrate function reference

The dehydrate function takes a queryClient and returns a serializable dehydrated state representation that can be embedded in markup and transported to the client for hydration.

HydrationBoundary component reference

HydrationBoundary is a component that wraps the component tree and accepts a state prop containing the dehydrated state from the server.

Next.js pages router full example with dehydration

In Next.js pages router, create a new QueryClient in getStaticProps or getServerSideProps, prefetch queries with await queryClient.prefetchQuery(), then return dehydrate(queryClient) in props. Wrap the page component with HydrationBoundary passing the dehydratedState prop.

Remix loader full example with dehydration

In Remix, create a new QueryClient in the loader function, prefetch queries with await queryClient.prefetchQuery(), then return json({ dehydratedState: dehydrate(queryClient) }). Access dehydratedState via useLoaderData and wrap components with HydrationBoundary.

Next.js pages router boilerplate reduction with top-level HydrationBoundary

To reduce boilerplate in Next.js pages router, place HydrationBoundary at the top level in _app.tsx wrapping the Component, passing pageProps.dehydratedState. This eliminates the need for a wrapper component in each route.

Caching markup in CDN with different staleTime

When caching markup in a CDN, set the cache time of the page itself high to avoid re-rendering on the server, but configure the staleTime of queries lower to ensure data is refetched in the background as soon as a user visits. For example, cache pages for a week but refetch data if older than a day.

dehydrate function purpose and basic usage

The dehydrate function creates a frozen representation of a cache that can later be hydrated with HydrationBoundary or hydrate. This is useful for passing prefetched queries from server to client or persisting queries to localStorage or other persistent locations. It only includes currently successful queries by default.

dehydrate function signature and parameters

The dehydrate function takes a QueryClient as the first required parameter and an optional DehydrateOptions object. The options object can include: shouldDehydrateMutation (function that determines whether to dehydrate mutations, defaults to only including paused mutations), shouldDehydrateQuery (function that determines whether to dehydrate queries, defaults to only including successful queries), serializeData (function to transform/serialize data during dehydration), and shouldRedactErrors (function to determine whether to redact errors, defaults to redacting all errors).

dehydrate return value format

The dehydrate function returns a DehydratedState object that includes everything needed to hydrate the queryClient at a later point. The exact format of this response is not part of the public API and can change at any time, so you should not rely on it. This result is not in serialized form—you need to serialize it yourself if desired.

dehydrate JSON serialization limitation

Some storage systems like browser Web Storage API require values to be JSON serializable. If you need to dehydrate values that are not automatically serializable to JSON (like Error or undefined), you must serialize them yourself. Since only successful queries are included by default, to also include Errors you must provide a shouldDehydrateQuery function that returns true.

hydrate function purpose

The hydrate function adds a previously dehydrated state into a cache.

hydrate function parameters

The hydrate function takes three parameters: client (required QueryClient to hydrate the state into), dehydratedState (required DehydratedState to hydrate into the client), and options (optional HydrateOptions object). The HydrateOptions can include defaultOptions (with mutations and queries properties for default options, and a deserializeData function) and queryClient (to use a custom QueryClient instead of the one from context).

hydrate data overwrite behavior

If the queries you are trying to hydrate already exist in the queryCache, hydrate will only overwrite them if the data is newer than the data present in the cache. Otherwise, it will not get applied.

HydrationBoundary component purpose

HydrationBoundary adds a previously dehydrated state into the queryClient that would be returned by useQueryClient(). If the client already contains data, the new queries will be intelligently merged based on update timestamp.

HydrationBoundary mutations limitation

Only queries can be dehydrated with an HydrationBoundary. Mutations cannot be included.

HydrationBoundary component parameters

HydrationBoundary takes a required state prop (DehydratedState) and optional props: options (HydrateOptions with defaultOptions for query options and queryClient for custom QueryClient), and queryClient (to use a custom QueryClient instead of the one from context).

defaultShouldDehydrateMutation and defaultShouldDehydrateQuery functions

These are importable functions that provide default behavior for shouldDehydrateMutation and shouldDehydrateQuery options in dehydrate. If you want to extend the function while retaining the default behavior, you should import and execute these as part of your return statement.

dehydrate example with custom error serialization

Example of dehydrating with custom serialization: On the server, call dehydrate(client, { shouldDehydrateQuery: () => true }) to include Errors, then call mySerialize(state) to transform Error instances to objects. On the client, call myDeserialize(serializedState) to transform objects back to Error instances, then call hydrate(client, state).

Give your agent this brain