Chapter 10

实战与最佳实践

用 queryKey factory + 自定义 hooks 组织一个完整 CRUD 应用,并沉淀性能优化清单与避坑指南。

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(默认,离线暂停)、alwaysofflineFirst。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 性能优化清单

10.8 常见坑对照

后果正解
queryKey 漏掉参数不同参数共用缓存,数据串所有依赖都进 key
fetch 不检查 res.ok错误被当成功缓存手动 throw 或用 axios
渲染里 new QueryClient每次渲染丢缓存组件外或 useState 惰性建
SSR 全局单例 client用户数据互相泄露每请求一个 client
服务端状态塞进 Redux双写、不同步交给 TanStack Query 管

10.9 全教程回顾

结语

TanStack Query 的价值不在"少写几行代码",而在把"服务端状态"这个长期被忽视的复杂领域,抽象成一套一致、可推理的心智模型。掌握它,你会发现前端状态管理豁然开朗——全局 store 瘦了,loading/error 不再是样板,数据永远和服务器"若即若离"地保持同步。祝你用得顺手。