SSR 与持久化
本章涵盖两个高级场景:服务端渲染(SSR)下的预取与注水(Hydration),以及客户端的离线持久化。
SSR 核心流程
服务端:
1. 创建 QueryClient
2. prefetchQuery() — 预取数据
3. dehydrate(queryClient) — 序列化缓存为 JSON
4. 将 dehydratedState 注入 HTML
客户端:
5. 从 HTML 读取 dehydratedState
6. queryClient.hydrate(dehydratedState) — 恢复缓存
7. useQuery 直接从缓存读取 → 无 loading 状态基础 SSR:dehydrate + HydrationBoundary
服务端(以 Next.js Pages Router 为例)
// pages/todos.tsx
import { dehydrate, QueryClient } from '@tanstack/react-query'
export async function getServerSideProps() {
const queryClient = new QueryClient()
// 预取数据(必须 await,确保数据就绪后再返回 HTML)
await queryClient.prefetchQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
return {
props: {
dehydratedState: dehydrate(queryClient),
},
}
}
function TodosPage({ dehydratedState }) {
return (
<HydrationBoundary state={dehydratedState}>
<TodoList /> {/* useQuery 命中服务端预取的缓存 */}
</HydrationBoundary>
)
}客户端
function TodoList() {
const { data, isPending } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
// 首次渲染时 data 来自服务端缓存的注水,isPending 为 false
}Next.js App Router(React Server Components)
v5 与 Next.js 13+ App Router 集成时,需要注意 RSC 和 Client Components 的分界:
方案一:在 Server Component 中预取,Client Component 中消费
// app/todos/page.tsx (Server Component)
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'
import { todoListOptions } from '@/query/options/todoOptions'
import TodoList from './TodoList' // Client Component
export default async function TodosPage() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery(todoListOptions())
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<TodoList />
</HydrationBoundary>
)
}// app/todos/TodoList.tsx (Client Component)
'use client'
import { useQuery } from '@tanstack/react-query'
import { todoListOptions } from '@/query/options/todoOptions'
export default function TodoList() {
const { data, isPending } = useQuery(todoListOptions())
// ...
}方案二:使用 useSuspenseQuery 避免注水不匹配
当 SSR 预取的数据与客户端首次渲染不一致时,会出现注水错误(Hydration Mismatch)。useSuspenseQuery 可以避免此问题:
'use client'
import { useSuspenseQuery } from '@tanstack/react-query'
function TodoList() {
const { data } = useSuspenseQuery(todoListOptions())
// data 永远不会是 undefined(Suspense 保证)
}
// 父组件用 Suspense 包裹
<Suspense fallback={<TodoListSkeleton />}>
<TodoList />
</Suspense>⚠️ 使用
useSuspenseQuery时,服务端retry默认为0(不重试),避免服务端渲染被阻塞。
服务端与客户端的 staleTime / gcTime 差异
| 配置 | 服务端 | 客户端 |
|---|---|---|
retry |
默认 0 |
默认 3 |
staleTime |
默认 0(或自定义) |
默认 0 |
gcTime |
默认 5min(无意义——服务端 QueryClient 在一次请求后即被销毁) |
默认 5min |
💡 最佳实践:服务端创建 QueryClient 时设置
staleTime大于客户端,这样预取的数据在客户端被视为 fresh,不会立即重取。例如:服务端staleTime: 60_000,客户端staleTime: 0。
Streaming SSR
React 18+ 支持 Streaming SSR(renderToPipeableStream)。TanStack Query v5 通过 useSuspenseQuery 原生支持:
// 使用 useSuspenseQuery 而非 useQuery
// Suspense 边界会触发流式渲染 —— 数据就绪的部分先发送给客户端
function App() {
return (
<Suspense fallback={<TodosSkeleton />}>
<Todos />
</Suspense>
)
}
function Todos() {
const { data } = useSuspenseQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
return <TodoList data={data} />
}prefetchQuery vs ensureQueryData vs fetchQuery
| 方法 | 行为 | 返回值 | 何时用 |
|---|---|---|---|
prefetchQuery |
如果缓存不存在或 stale,发起请求 | Promise<void> |
SSR、路由预取 |
ensureQueryData |
如果缓存不存在,发起请求;如果存在且 fresh,不请求 | Promise<TData> |
需要确认数据存在且获取它 |
fetchQuery |
无论如何都发起请求 | Promise<TData> |
需要强制获取最新数据 |
持久化(Offline Persistence)
@tanstack/react-query-persist-client 支持将缓存持久化到 localStorage / IndexedDB / AsyncStorage,实现离线体验。
安装
npm install @tanstack/react-query-persist-client配置(localStorage)
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
import { createSyncStoragePersister } from '@tanstack/react-query-persist-client'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 小时(离线场景需要更长的 gcTime)
staleTime: 5 * 60 * 1000,
},
},
})
const persister = createSyncStoragePersister({
storage: window.localStorage,
// 可选:只持久化部分查询
key: 'REACT_QUERY_OFFLINE_CACHE',
throttleTime: 1000, // 1 秒内最多写入一次
})
function App() {
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister }}
>
<YourApp />
</PersistQueryClientProvider>
)
}持久化配置选项
<PersistQueryClientProvider
client={queryClient}
persistOptions={{
persister,
maxAge: 1000 * 60 * 60 * 24, // 超过 24h 的缓存不恢复
buster: 'v2', // 版本升级时清除旧缓存
dehydrateOptions: {
shouldDehydrateQuery: (query) => {
// 只持久化特定查询
return query.queryKey[0] !== 'transient'
},
},
}}
>注意事项
| 问题 | 解决方案 |
|---|---|
| localStorage 容量限制(5MB) | 只持久化关键查询,设置 maxAge 清除旧数据 |
| 版本升级导致缓存结构不兼容 | 修改 buster 字符串强制清除 |
| 敏感数据不应持久化 | 用 shouldDehydrateQuery 过滤 |
服务端无 window.localStorage |
检查 typeof window !== 'undefined',服务端用 noop persister |
多标签页同步
@tanstack/react-query-broadcast-client-experimental 通过 Broadcast Channel API 同步多个标签页的缓存:
import { broadcastQueryClient } from '@tanstack/react-query-broadcast-client-experimental'
broadcastQueryClient({
queryClient,
broadcastChannel: 'my-app',
})当一个标签页执行 mutation 成功并 invalidate 查询后,其他标签页的相同查询也会自动重取。
离线 Mutation 队列
结合 networkMode: 'offlineFirst' 和持久化,可以实现离线 mutation 队列:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
networkMode: 'offlineFirst', // 离线时返回缓存
},
mutations: {
networkMode: 'offlineFirst', // 离线时 mutation 暂停,联网后自动重试
},
},
})🔬 深入原理:TanStack Query 内部有一个
onlineManager,监听window.addEventListener('online'/'offline')。当网络恢复在线时,暂停的 queries 和 mutations 自动恢复。这是通过@tanstack/query-core中的OnlineManager类实现的。
完整的 SSR + 持久化架构
用户首次访问
│
▼
服务端:prefetchQuery → dehydrate → 序列化到 HTML
│
▼
客户端:HydrationBoundary → hydrate → useQuery 从缓存读
│
▼
后续交互:正常使用 useQuery / useMutation
│
▼
缓存变更 → persistClient → 写入 localStorage
│
▼
用户关闭页面后重新打开
│
▼
persistClient → 从 localStorage 恢复缓存 → 即时显示旧数据 → 后台重取