Chapter 09

SSR 与框架集成

在 Next.js 服务端预取数据并注水到客户端,同时了解 Vue Query / Solid Query 的用法差异。

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 各框架差异速览

框架安装/入口返回形态
ReactQueryClientProvider普通对象 { data, ... }
Vue 3VueQueryPluginref(.value / 模板自动解包)
SolidQueryClientProvider响应式访问器 query.data
SvelteQueryClientProviderstore($query)
学一次,处处通

queryKey、staleTime、gcTime、invalidateQueries、useMutation 等核心概念在所有框架里完全一致,只是"响应式载体"随框架不同(对象/ref/signal/store)。前八章的知识可无缝迁移。

9.8 小结与下一步