10.1 从零件到工程
前九章讲透了每个 API,本章把它们组织成可维护的项目结构:集中管理 queryKey、封装领域自定义 hooks、用 DevTools 调试,并给出一份可直接照做的优化清单。
10.2 核心概念
- queryKey factory
- 把某领域所有 queryKey 集中在一个对象里生成,如
todoKeys.detail(id)。避免全项目散落的字符串数组拼错,失效时也能精准定位层级。 - 自定义 hooks 封装
- 把 useQuery/useMutation 包成
useTodos()/useAddTodo()领域 hooks,组件只管调用,key、fn、失效逻辑都藏在 hooks 内——组件极简、逻辑可复用可测。 - ReactQueryDevtools
- 开发面板,实时展示每个 query 的 key、状态(fresh/stale/fetching)、数据、观察者数量,能手动触发 refetch/invalidate,是调试缓存行为的利器。
- 结构共享 (Structural Sharing)
- TanStack Query 默认对新旧数据做深比较,未变的部分保持同一引用,从而减少不必要的重渲染——这也是为何要保证 queryFn 返回可比较的普通对象。
- 网络模式 (networkMode)
- 控制离线行为:
online(默认,离线暂停)、always、offlineFirst。PWA/离线应用需按需配置。 - persistQueryClient
- 把缓存持久化到 localStorage/IndexedDB,刷新页面或离线后仍能秒显上次数据,配合 maxAge 控制过期。
- 常见坑
- 如 queryKey 不含全部依赖导致数据串、fetch 不 throw 导致错误被吞、SSR 用全局 client、在渲染里 new QueryClient、把服务端状态又塞进 Redux 造成双写。
10.3 queryKey factory
// queries/todo-keys.ts
export const todoKeys = {
all: ["todos"] as const,
lists: () => [...todoKeys.all, "list"] as const,
list: (filters: Filters) => [...todoKeys.lists(), filters] as const,
details: () => [...todoKeys.all, "detail"] as const,
detail: (id: number) => [...todoKeys.details(), id] as const,
};
// 失效整个领域 / 只失效列表 / 只失效某详情,全从一处来
qc.invalidateQueries({ queryKey: todoKeys.all });
qc.invalidateQueries({ queryKey: todoKeys.lists() });
qc.invalidateQueries({ queryKey: todoKeys.detail(5) });
10.4 自定义 hooks 封装
// hooks/use-todos.ts
export function useTodos(filters: Filters) {
return useQuery({
queryKey: todoKeys.list(filters),
queryFn: () => fetchTodos(filters),
staleTime: 30_000,
});
}
export function useAddTodo() {
const qc = useQueryClient();
return useMutation({
mutationFn: createTodo,
onSuccess: () => qc.invalidateQueries({ queryKey: todoKeys.lists() }),
});
}
// 组件里干干净净
function TodoPage() {
const { data, isLoading } = useTodos({ status: "open" });
const add = useAddTodo();
// … 渲染 + add.mutate(...)
}
10.5 完整 CRUD 变更 hooks
export function useUpdateTodo() {
const qc = useQueryClient();
return useMutation({
mutationFn: updateTodo,
onMutate: async (next) => {
await qc.cancelQueries({ queryKey: todoKeys.detail(next.id) });
const prev = qc.getQueryData(todoKeys.detail(next.id));
qc.setQueryData(todoKeys.detail(next.id), next);
return { prev };
},
onError: (_e, next, ctx) =>
qc.setQueryData(todoKeys.detail(next.id), ctx?.prev),
onSettled: (_d, _e, next) =>
qc.invalidateQueries({ queryKey: todoKeys.detail(next.id) }),
});
}
export function useDeleteTodo() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: number) => deleteTodo(id),
onSuccess: () => qc.invalidateQueries({ queryKey: todoKeys.lists() }),
});
}
10.6 接入 DevTools
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
<QueryClientProvider client={qc}>
<App />
{process.env.NODE_ENV === "development" && (
<ReactQueryDevtools initialIsOpen={false} />
)}
</QueryClientProvider>
DevTools 是学习加速器
打开面板,随手切换标签页、点按钮,观察每个 query 在 fresh → stale → fetching 之间跳变,能让第 3 章的缓存机制从"文字"变成"肉眼可见"。生产环境记得按 NODE_ENV 排除。
10.7 性能优化清单
- 合理设
staleTime:静态数据 Infinity,业务数据 1~5min,别让默认 0 疯狂重取。 - 用
select派生切片,减少无关字段变化引发的重渲染。 - 用
queryOptions+ queryKey factory 统一配置,杜绝 key 拼错。 - 列表→详情用
prefetchQuery在 hover / 路由前预取。 - 大列表用虚拟滚动 + useInfiniteQuery,别一次渲染上万节点。
- 乐观更新前
cancelQueries,避免旧响应覆盖。 - 需要离线秒开时用
persistQueryClient。 - 关闭对表单页不必要的
refetchOnWindowFocus。
10.8 常见坑对照
| 坑 | 后果 | 正解 |
|---|---|---|
| queryKey 漏掉参数 | 不同参数共用缓存,数据串 | 所有依赖都进 key |
| fetch 不检查 res.ok | 错误被当成功缓存 | 手动 throw 或用 axios |
| 渲染里 new QueryClient | 每次渲染丢缓存 | 组件外或 useState 惰性建 |
| SSR 全局单例 client | 用户数据互相泄露 | 每请求一个 client |
| 服务端状态塞进 Redux | 双写、不同步 | 交给 TanStack Query 管 |
10.9 全教程回顾
- 服务端状态需要专门工具:缓存、去重、后台同步(Ch1)。
- useQuery 三要素 + 缓存机制 staleTime/gcTime 是根基(Ch2-3)。
- 分页/无限加载、mutation/乐观更新、失效/预取组成数据流全貌(Ch4-6)。
- 错误处理、高级组合、SSR 让应用走向生产(Ch7-9)。
- queryKey factory + 自定义 hooks 是可维护工程的关键(Ch10)。
结语
TanStack Query 的价值不在"少写几行代码",而在把"服务端状态"这个长期被忽视的复杂领域,抽象成一套一致、可推理的心智模型。掌握它,你会发现前端状态管理豁然开朗——全局 store 瘦了,loading/error 不再是样板,数据永远和服务器"若即若离"地保持同步。祝你用得顺手。