9.1 SSR 为什么要特别处理
服务端渲染时,你希望 HTML 里就带着数据(利于 SEO、首屏快),而不是发到浏览器再空转一次 loading。TanStack Query 的方案是:服务端预取 → 序列化缓存 → 客户端注水,让客户端"接住"服务端已经拿到的数据,不重复请求。
9.2 核心概念
- dehydrate (脱水)
- 把服务端 QueryClient 里的缓存序列化成可传输的普通对象(JSON 友好),随 HTML 一起发给浏览器。
- hydrate / HydrationBoundary (注水)
- 客户端把脱水的数据填回自己的 QueryClient。
<HydrationBoundary state={dehydratedState}>包裹的子树里,useQuery 会直接命中这份缓存,跳过首次请求。 - prefetchQuery (服务端)
- 在服务端组件/loader 里提前拉数据进 QueryClient,之后脱水带走。它不 throw、只填缓存。
- 每请求一个 QueryClient
- SSR 场景下不能用全局单例 QueryClient,否则多个用户的数据会串。必须每次请求 new 一个,或用
cache()(React) 保证请求级单例。 - streaming SSR
- Next.js App Router 支持流式:服务端边渲染边把 dehydrate 的数据分块发送,配合 Suspense 实现渐进式注水。
- 框架适配包
- vue-query 用
VueQueryPlugin安装、Composition API 风格;solid-query 返回 signal 式访问器;核心概念(queryKey/staleTime/mutation)完全一致。 - initialData vs 注水
- 小数据可直接给 useQuery 传 initialData;成规模的 SSR 用 dehydrate/HydrationBoundary 更系统、能带上 dataUpdatedAt 等元信息。
9.3 Next.js App Router 集成
先做一个"每请求单例"的 QueryClient 工厂 + 客户端 Provider:
// app/get-query-client.ts
import { QueryClient, isServer } from "@tanstack/react-query";
function makeClient() {
return new QueryClient({
defaultOptions: { queries: { staleTime: 60_000 } },
});
}
let browserClient: QueryClient | undefined;
export function getQueryClient() {
if (isServer) return makeClient(); // 服务端每次新建
return (browserClient ??= makeClient()); // 浏览器复用
}
// app/providers.tsx
"use client";
import { QueryClientProvider } from "@tanstack/react-query";
import { getQueryClient } from "./get-query-client";
export function Providers({ children }) {
const qc = getQueryClient();
return <QueryClientProvider client={qc}>{children}</QueryClientProvider>;
}
9.4 服务端预取 + 注水
// app/todos/page.tsx —— 服务端组件
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
import { getQueryClient } from "../get-query-client";
import { todosQuery } from "@/queries/todo";
export default async function Page() {
const qc = getQueryClient();
await qc.prefetchQuery(todosQuery()); // 服务端拉数据
return (
<HydrationBoundary state={dehydrate(qc)}>
<Todos /> {/* 客户端组件,useQuery 直接命中缓存 */}
</HydrationBoundary>
);
}
SEO + 无闪烁
这样服务端 HTML 已含 todos 数据(利于 SEO),客户端 <Todos> 里的 useQuery 首次渲染就是 success 状态,不会闪 loading——之后按 staleTime 决定是否后台刷新。
千万别用全局 QueryClient
SSR 下若在模块顶层 new QueryClient() 作全局单例,所有请求共享同一缓存 → 用户 A 会看到用户 B 的数据,严重安全问题。务必"每个服务端请求一个 client"。
9.5 Vue Query
// main.ts
import { VueQueryPlugin } from "@tanstack/vue-query";
app.use(VueQueryPlugin);
// 组件 <script setup>
import { useQuery } from "@tanstack/vue-query";
const { data, isLoading } = useQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
});
// data 是 ref,模板里直接 {{ data }};key 可传 ref 自动响应
9.6 Solid Query
import { createQuery } from "@tanstack/solid-query";
const query = createQuery(() => ({
queryKey: ["todos"],
queryFn: fetchTodos,
}));
// 访问器:query.data / query.isLoading(Solid 的响应式)
<Show when={!query.isLoading}>{JSON.stringify(query.data)}</Show>
9.7 各框架差异速览
| 框架 | 安装/入口 | 返回形态 |
|---|---|---|
| React | QueryClientProvider | 普通对象 { data, ... } |
| Vue 3 | VueQueryPlugin | ref(.value / 模板自动解包) |
| Solid | QueryClientProvider | 响应式访问器 query.data |
| Svelte | QueryClientProvider | store($query) |
学一次,处处通
queryKey、staleTime、gcTime、invalidateQueries、useMutation 等核心概念在所有框架里完全一致,只是"响应式载体"随框架不同(对象/ref/signal/store)。前八章的知识可无缝迁移。
9.8 小结与下一步
- SSR = 服务端 prefetch → dehydrate → 客户端 HydrationBoundary 注水。
- Next.js App Router 下务必"每请求一个 QueryClient",切忌全局单例。
- 注水后客户端首屏即 success,无闪烁且利于 SEO。
- Vue/Solid/Svelte 概念一致,仅响应式载体不同。
- 最后一章 实战与最佳实践——完整 CRUD、queryKey factory、自定义 hooks、优化清单。