# ssr-dehydration: Use Dehydrate/Hydrate Pattern for SSR ## Priority: MEDIUM ## Explanation For server-side rendering, prefetch queries on the server, dehydrate the cache to a serializable format, send it to the client, and hydrate on the client. This prevents content flash and duplicate requests. ## Bad Example ```tsx // No SSR data passing - client refetches everything // server-side export async function getServerSideProps() { const data = await fetchPosts() return { props: { posts: data } } // Bypasses React Query cache } // client-side function PostsPage({ posts }: { posts: Post[] }) { // This doesn't benefit from the server fetch const { data } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts, // Will refetch on client, causing flash }) return // Awkward fallback pattern } ``` ## Good Example: Next.js App Router ```tsx // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import { postQueries } from '@/lib/queries' export default async function PostsPage() { const queryClient = new QueryClient() await queryClient.prefetchQuery(postQueries.list()) return ( ) } // components/PostList.tsx 'use client' import { useSuspenseQuery } from '@tanstack/react-query' import { postQueries } from '@/lib/queries' export function PostList() { const { data: posts } = useSuspenseQuery(postQueries.list()) return ( ) } ``` ## Good Example: TanStack Start/Router ```tsx // routes/posts.tsx import { createFileRoute } from '@tanstack/react-router' import { postQueries } from '@/lib/queries' export const Route = createFileRoute('/posts')({ loader: async ({ context: { queryClient } }) => { // Prefetch in route loader await queryClient.ensureQueryData(postQueries.list()) }, component: PostsPage, }) function PostsPage() { const { data: posts } = useSuspenseQuery(postQueries.list()) return } ``` ## Good Example: Manual SSR Setup ```tsx // server.tsx import { dehydrate, QueryClient } from '@tanstack/react-query' import { renderToString } from 'react-dom/server' export async function render(url: string) { const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 60 * 1000, // Prevent immediate client refetch }, }, }) // Prefetch required data await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: fetchPosts, }) const dehydratedState = dehydrate(queryClient) const html = renderToString( ) // Serialize safely - JSON.stringify is XSS vulnerable const serializedState = serialize(dehydratedState) return `
${html}
` } // client.tsx import { hydrate, QueryClient, QueryClientProvider } from '@tanstack/react-query' const queryClient = new QueryClient() hydrate(queryClient, window.__DEHYDRATED_STATE__) hydrateRoot( document.getElementById('app'), ) ``` ## Context - Create new QueryClient per request to prevent data sharing between users - Set `staleTime > 0` on server to prevent immediate client refetch - Use a safe serializer (not JSON.stringify) to prevent XSS - Failed queries aren't dehydrated by default; use `shouldDehydrateQuery` to override - `HydrationBoundary` can be nested for route-level prefetching