TanStack 基础知识参考字典

TanStack 家族条目式字典:Query v5 / Table v9 / Router / Form。每条给语义、示例、边界与坑。

1. useQuery – 服务端状态基础

描述

声明式取数:queryKey 是身份(键变即重新请求),queryFn 是取数函数。v5 用 isPending 替代 isLoading,error 类型为 Error|null。

语法和示例

const { data, isPending, isError, error, refetch } = useQuery({
  queryKey: ['posts', { page }],
  queryFn: () => fetch(`/api/posts?page=${page}`)
    .then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }),
  staleTime: 60_000,      // 新鲜期:期内不重取
  gcTime: 5 * 60_000,     // 缓存保留期(v4 叫 cacheTime)
  retry: 2,
  enabled: !!userId,      // 条件请求
});
// 只订阅切片(减少重渲染)
useQuery({ ...opts, select: (d) => d.items });

注意事项

  • queryKey 里的对象是结构比较(键顺序无关),但函数/类实例不参与比较
  • queryFn 抛错才会进 isError;返回 4xx 响应不会自动抛(要自己 if (!r.ok) throw)
  • staleTime 默认为 0,意味着窗口聚焦、组件挂载都会重取——多数场景该设 30-60 秒
  • gcTime 小于 staleTime 会导致数据被回收后重新请求
  • queryKey 数组里避免放不稳定的值(如每次新建的对象字面量内容变化)

2. useMutation – 变更与缓存失效

描述

写操作封装:mutationFn 执行、onSuccess 失效相关查询、onError 回滚。配套乐观更新是 v5 的核心能力。

语法和示例

const qc = useQueryClient();
const create = useMutation({
  mutationFn: (body: NewPost) => fetch('/api/posts', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(body),
  }).then(r => r.json()),
  onSuccess: () => {
    qc.invalidateQueries({ queryKey: ['posts'] });
  },
  onError: (err) => toast(err.message),
});
create.mutate(payload);
create.mutateAsync(payload);   // 返回 Promise 便于 await

注意事项

  • invalidateQueries 只是标记过期并触发在用的查询重取,不会立即拿新数据
  • onSuccess 里拿不到最新的列表数据——需要旧值用 onMutate + getQueryData
  • mutationFn 抛错才会进 onError;返回错误状态码要自己 throw
  • mutate 与 mutateAsync 的区别是后者返回 Promise(try/catch 可用)
  • 并发多次 mutate 时 onSuccess 可能乱序,需要顺序用 mutateAsync 串行

3. 乐观更新完整套路

描述

onMutate 先改本地缓存并快照旧值 → onError 回滚 → onSettled 无论成败都失效重取。

语法和示例

const update = useMutation({
  mutationFn: (next: Todo) => patchTodo(next),
  onMutate: async (next) => {
    await qc.cancelQueries({ queryKey: ['todos'] });   // 取消在途请求
    const prev = qc.getQueryData<Todo[]>(['todos']);
    qc.setQueryData<Todo[]>(['todos'], (old) =>
      (old ?? []).map(t => t.id === next.id ? next : t));
    return { prev };                                   // 上下文传给回调
  },
  onError: (_err, _next, ctx) => {
    qc.setQueryData(['todos'], ctx?.prev);             // 回滚
  },
  onSettled: () => qc.invalidateQueries({ queryKey: ['todos'] }),
});

注意事项

  • 必须先 cancelQueries,否则在途请求返回会覆盖乐观更新
  • onMutate 返回的上下文只能传给同一 mutation 的 onError/onSettled
  • 回滚只回滚这一个 queryKey,关联的其他查询需自行处理
  • onSettled 里再失效是兜底(服务端可能返回与预期不同的数据)
  • 乐观更新对列表分页/排序复杂场景要谨慎(局部替换可能错位)

4. useInfiniteQuery – 无限加载

描述

分页数据的专用 Hook:initialPageParam 起手、getNextPageParam 决定下一参数(null 终止)、data.pages 是二维数组。

语法和示例

const q = useInfiniteQuery({
  queryKey: ['feed'],
  queryFn: ({ pageParam }) => fetch(`/feed?cursor=${pageParam}`).then(r => r.json()),
  initialPageParam: 0,
  getNextPageParam: (last, all) => last.nextCursor ?? null,
});
q.data?.pages.flatMap(p => p.items);
q.hasNextPage; q.isFetchingNextPage;
q.fetchNextPage();
// 双向加载
getPreviousPageParam: (first) => first.prevCursor ?? null;

注意事项

  • getNextPageParam 返回 undefined 会被当成还有下一页(必须显式 null)
  • data.pages 是二维数组,渲染前要 flatMap(别直接 map)
  • queryKey 变化会重置所有已加载页——筛选条件应放进 key
  • maxPages 可限制内存(超出时丢弃最远页)
  • React Native 的 FlatList 里配 onEndReached 触发 fetchNextPage

5. Table v9 – 入口与特性组合

描述

v9 重构:useTable 取代 useReactTable,tableFeatures 必填。行模型与排序函数必须在 features 里显式注册(不再隐式可用)。

语法和示例

import { useTable, tableFeatures, rowSortingFeature,
  createSortedRowModel, sortFns } from '@tanstack/react-table';

const features = tableFeatures({
  rowSortingFeature,
  sortedRowModel: createSortedRowModel(),
  sortFns: { alphanumeric: sortFns.alphanumeric, basic: sortFns.basic },
});
function Persons({ data }: { data: Person[] }) {
  const columns = useMemo(() => buildColumns(), []);   // 纯常量放外层
  const table = useTable({ features, data, columns });
  return (
    <table>
      <thead>{table.getHeaderGroups().map(hg => (
        <tr key={hg.id}>{hg.headers.map(h => (
          <th key={h.id} onClick={h.column.getToggleSortingHandler()}>
            {flexRender(h.column.columnDef.header, h.getContext())}
          </th>))}</tr>))}</thead>
      <tbody>{table.getRowModel().rows.map(row => (
        <tr key={row.id}>{row.getVisibleCells().map(c => (
          <td key={c.id}>{flexRender(c.column.columnDef.cell, c.getContext())}</td>))}</tr>))}</tbody>
    </table>
  );
}

注意事项

  • features 是必填项——v8 的隐式能力在 v9 全要显式声明
  • 列定义函数必须放组件外或 useMemo,否则每次渲染重建导致状态丢失
  • 行模型(sorted/filtered/paginated)要按需注册,注册顺序影响管线顺序
  • flexRender 才会执行 cell/header 里的函数,直接渲染会显示函数源码
  • getRowModel() 是当前视图的行,getPrePaginationRowModel 等是各阶段快照

6. columnHelper – 列定义

描述

类型安全的列工厂:accessor 声明数据源,header/cell 是渲染函数,可加排序/过滤/列宽等元数据。

语法和示例

const columnHelper = createColumnHelper<Person>();

const buildColumns = () => columnHelper.columns([
  columnHelper.accessor('name', { header: '姓名', size: 120 }),
  columnHelper.accessor('age', {
    header: '年龄',
    cell: (info) => `${info.getValue()} 岁`,
    sortingFn: 'alphanumeric',
  }),
  columnHelper.accessor((row) => row.role, {   // accessorFn 派生
    id: 'role',
    header: '角色',
  }),
  columnHelper.display({                        // 无数据源的展示列
    id: 'actions',
    cell: ({ row }) => <button onClick={() => del(row.original.id)}>删</button>,
  }),
]);

注意事项

  • accessorFn 必须给 id(否则无法作为 key)
  • display 列没有 getValue,只能用 row.original 取数据
  • sortingFn 字符串名必须在 features.sortFns 注册过
  • 列定义里的函数会被多次调用,别在其中做副作用
  • 变更列定义(条件判断)会让表格重建并丢失状态

7. 排序交互与状态

描述

表头 getToggleSortingHandler 切换 asc→desc→none;排序状态可由 table 内部管理(initialState)或外部受控。

语法和示例

<th onClick={h.column.getToggleSortingHandler()}>
  {flexRender(h.column.columnDef.header, h.getContext())}
  {{ asc: ' ↑', desc: ' ↓' }[h.column.getIsSorted() as string] ?? ''}
</th>
// 受控排序(与服务端排序联动)
const [sorting, setSorting] = useState<SortingState>([]);
const table = useTable({ features, data, columns,
  state: { sorting }, onSortingChange: setSorting });
// 服务端排序:把 sorting 转成 query 参数并放进 queryKey

注意事项

  • getIsSorted() 返回 false | ‘asc’ | ‘desc’,用对象索引前要收窄类型
  • 受控模式(state)与服务端排序配合时,data 应是已排序的结果(别本地再排)
  • 多列排序需 columnDef.enableMultiSort = true
  • 排序函数对 null/undefined 的处理要显式(否则顺序不可预期)
  • 手动排序(manualSorting)时不要注册 sortedRowModel

8. Router – 路由树与类型安全

描述

createRoute 声明式拼树,类型从父路由推导;文件式路由用 router-plugin 生成。beforeLoad 做守卫、loader 做取数。

语法和示例

const root = createRootRoute();
const posts = createRoute({
  getParentRoute: () => root,
  path: '/posts',
  loader: () => fetchPosts(),
  component: Posts,
});
const detail = createRoute({
  getParentRoute: () => posts,
  path: '$id',
  loader: ({ params }) => fetchPost(params.id),
});
const tree = root.addChildren([posts.addChildren([detail])]);
const router = createRouter({ routeTree: tree });
// 嵌在已有 Provider 内
<QueryClientProvider>
  <RouterProvider router={router} />
</QueryClientProvider>

注意事项

  • Router 必须嵌在其他 Provider 内部(否则拿不到 QueryClient)
  • loader 返回的数据经 useLoaderData 获取,不是 useState 存的
  • beforeLoad 抛 redirect() 做守卫,不要用 navigate 手动跳
  • 路由树变更后需重新生成类型(文件式路由有代码生成步骤)
  • params 的 id 是 string(URL 参数永远是字符串),要转数字自己转

9. Form v1 – 无状态表单

描述

v1 重写为无状态设计:不使用内部 store,靠 field API 读写值。校验支持 Standard Schema(Zod/Valibot)或函数。

语法和示例

const form = useForm({
  defaultValues: { email: '', age: 0 },
  validators: {
    onSubmit: zodSchema,                       // 或 ({ value }) => errors
    onChange: ({ value }) => value.age < 0 ? '年龄非法' : undefined,
  },
});
<form.Field name="email">
  {(field) => (
    <>
      <input value={field.state.value}
        onChange={(e) => field.handleChange(e.target.value)} />
      {field.state.meta.errors.join(', ')}
    </>
  )}
</form.Field>
// 提交态
<form.Subscribe selector={(s) => [s.canSubmit, s.isSubmitting]}>
  {([ok, busy]) => <button disabled={!ok || busy}>提交</button>}
</form.Subscribe>

注意事项

  • 字段错误在 field.state.meta.errors,表单级错误在 form.state.errors——别混
  • canSubmit 已包含校验与 pending 状态,提交按钮只看它即可
  • v1 不再有 form.state.values 之外的隐式 store,读值统一用 form.getFieldValue
  • onChange 校验每次输入都跑,重校验用 onChangeAsync + debounce
  • 数组字段用 form.Field 的 mode=’array’,不是 FieldArray 组件(v0 的写法)
上一篇
下一篇