initial commit
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
# 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 <PostList posts={data ?? posts} /> // 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 (
|
||||
<HydrationBoundary state={dehydrate(queryClient)}>
|
||||
<PostList />
|
||||
</HydrationBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<ul>
|
||||
{posts.map(post => (
|
||||
<li key={post.id}>{post.title}</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 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 <PostList posts={posts} />
|
||||
}
|
||||
```
|
||||
|
||||
## 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(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
</QueryClientProvider>
|
||||
)
|
||||
|
||||
// Serialize safely - JSON.stringify is XSS vulnerable
|
||||
const serializedState = serialize(dehydratedState)
|
||||
|
||||
return `
|
||||
<html>
|
||||
<body>
|
||||
<div id="app">${html}</div>
|
||||
<script>window.__DEHYDRATED_STATE__ = ${serializedState}</script>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
}
|
||||
|
||||
// client.tsx
|
||||
import { hydrate, QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
hydrate(queryClient, window.__DEHYDRATED_STATE__)
|
||||
|
||||
hydrateRoot(
|
||||
document.getElementById('app'),
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
</QueryClientProvider>
|
||||
)
|
||||
```
|
||||
|
||||
## 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
|
||||
Reference in New Issue
Block a user