> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# TanStack Table / Form / Virtual 完全指南
- URL: https://blog.vercanti.com/tanstack-table-form-virtual-wan-quan-zhi-nan/
- Published: 2026-08-28T14:35:21.000Z
- Updated: 2026-08-28T14:58:38.000Z
- Description: 最后更新：2026-03-29 TanStack Table 是一个无头（Headless）表格库，只提供逻辑和状态管理，不提供任何 UI 样式，完全由开发者控制渲染。支持 React、Vue、Solid、Svelte。 无头的含义：Table 的核心是一个状态管理器，你传入数据和列定义，它返回处理好的行、列、单元格数据和操作方法，具体如何渲染成 HTML 完全自定义。 TanStack Form 是一个无头、类型安全的表单状态管理库，支持同步/异步验证，支持 React、Vue、Solid 等。 与 React Hook Form 对比： TanSta
- Author: yellowdog
- Tags: 前端开发, TanStack

最后更新：2026-03-29

> 官方文档：<https://tanstack.com/table/latest> | <https://tanstack.com/form/latest> | <https://tanstack.com/virtual/latest>  
> 适用版本：TanStack Table/Form/Virtual v8（2026-05-07 核实）

---

# 一、TanStack Table v8

## 1\. 基础概念

### TanStack Table 是什么

TanStack Table 是一个无头（Headless）表格库，只提供逻辑和状态管理，不提供任何 UI 样式，完全由开发者控制渲染。支持 React、Vue、Solid、Svelte。

**无头的含义**：Table 的核心是一个状态管理器，你传入数据和列定义，它返回处理好的行、列、单元格数据和操作方法，具体如何渲染成 HTML 完全自定义。

### 安装

```bash
npm install @tanstack/react-table
# Vue
npm install @tanstack/vue-table

```

---

## 2\. 快速开始

### 列定义（ColumnDef）

| 属性                 | 类型                           | 说明                     |
| ------------------ | ---------------------------- | ---------------------- |
| accessorKey        | string                       | 从数据对象取值的字段名            |
| accessorFn         | (row) => value               | 自定义取值函数                |
| id                 | string                       | 列唯一 ID（accessorFn 时必填） |
| header             | string \| (ctx) => ReactNode | 表头内容                   |
| cell               | (ctx) => ReactNode           | 单元格渲染                  |
| footer             | string \| (ctx) => ReactNode | 表尾内容                   |
| enableSorting      | boolean                      | 是否允许排序                 |
| enableColumnFilter | boolean                      | 是否允许列过滤                |
| size               | number                       | 列宽                     |
| minSize / maxSize  | number                       | 最小/最大列宽                |
| meta               | object                       | 自定义元数据                 |

### 基础示例（React）

```tsx
import {
  createColumnHelper,
  flexRender,
  getCoreRowModel,
  useReactTable,
} from "@tanstack/react-table";

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

const columnHelper = createColumnHelper<User>();

const columns = [
  columnHelper.accessor("id", {
    header: "ID",
    cell: (info) => info.getValue(),
  }),
  columnHelper.accessor("name", {
    header: "姓名",
    cell: (info) => <strong>{info.getValue()}</strong>,
  }),
  columnHelper.accessor("email", {
    header: "邮箱",
  }),
  columnHelper.accessor("age", {
    header: "年龄",
    cell: (info) => `${info.getValue()} 岁`,
  }),
  // 操作列（无数据字段）
  columnHelper.display({
    id: "actions",
    header: "操作",
    cell: ({ row }) => (
      <button onClick={() => handleDelete(row.original.id)}>删除</button>
    ),
  }),
];

function UserTable({ data }: { data: User[] }) {
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  });

  return (
    <table>
      <thead>
        {table.getHeaderGroups().map((headerGroup) => (
          <tr key={headerGroup.id}>
            {headerGroup.headers.map((header) => (
              <th key={header.id}>
                {flexRender(header.column.columnDef.header, header.getContext())}
              </th>
            ))}
          </tr>
        ))}
      </thead>
      <tbody>
        {table.getRowModel().rows.map((row) => (
          <tr key={row.id}>
            {row.getVisibleCells().map((cell) => (
              <td key={cell.id}>
                {flexRender(cell.column.columnDef.cell, cell.getContext())}
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

```

---

## 3\. 排序

```tsx
import { getSortedRowModel, SortingState } from "@tanstack/react-table";
import { useState } from "react";

const [sorting, setSorting] = useState<SortingState>([]);

const table = useReactTable({
  data,
  columns,
  state: { sorting },
  onSortingChange: setSorting,
  getCoreRowModel: getCoreRowModel(),
  getSortedRowModel: getSortedRowModel(),
});

// 表头渲染（点击切换排序）
{table.getHeaderGroups().map((headerGroup) => (
  <tr key={headerGroup.id}>
    {headerGroup.headers.map((header) => (
      <th
        key={header.id}
        onClick={header.column.getToggleSortingHandler()}
        style={{ cursor: header.column.getCanSort() ? "pointer" : "default" }}
      >
        {flexRender(header.column.columnDef.header, header.getContext())}
        {{
          asc: " ↑",
          desc: " ↓",
        }[header.column.getIsSorted() as string] ?? ""}
      </th>
    ))}
  </tr>
))}

```

---

## 4\. 分页

```tsx
import { getPaginationRowModel, PaginationState } from "@tanstack/react-table";

const [pagination, setPagination] = useState<PaginationState>({
  pageIndex: 0,
  pageSize: 10,
});

const table = useReactTable({
  data,
  columns,
  state: { pagination },
  onPaginationChange: setPagination,
  getCoreRowModel: getCoreRowModel(),
  getPaginationRowModel: getPaginationRowModel(),
  // 服务端分页时设置总行数
  rowCount: totalCount,
  manualPagination: true,  // 服务端分页时开启
});

// 分页控件
<div>
  <button onClick={() => table.firstPage()} disabled={!table.getCanPreviousPage()}>首页</button>
  <button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>上一页</button>
  <span>{table.getState().pagination.pageIndex + 1} / {table.getPageCount()}</span>
  <button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>下一页</button>
  <button onClick={() => table.lastPage()} disabled={!table.getCanNextPage()}>末页</button>
  <select
    value={table.getState().pagination.pageSize}
    onChange={(e) => table.setPageSize(Number(e.target.value))}
  >
    {[10, 20, 50, 100].map((size) => (
      <option key={size} value={size}>每页 {size} 条</option>
    ))}
  </select>
</div>

```

---

## 5\. 过滤

```tsx
import {
  getFilteredRowModel,
  ColumnFiltersState,
} from "@tanstack/react-table";

const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [globalFilter, setGlobalFilter] = useState("");

const table = useReactTable({
  data,
  columns,
  state: { columnFilters, globalFilter },
  onColumnFiltersChange: setColumnFilters,
  onGlobalFilterChange: setGlobalFilter,
  getCoreRowModel: getCoreRowModel(),
  getFilteredRowModel: getFilteredRowModel(),
});

// 全局搜索框
<input
  placeholder="搜索..."
  value={globalFilter}
  onChange={(e) => setGlobalFilter(e.target.value)}
/>

// 列过滤输入框（表头下方）
{header.column.getCanFilter() && (
  <input
    value={(header.column.getFilterValue() as string) ?? ""}
    onChange={(e) => header.column.setFilterValue(e.target.value)}
    placeholder="过滤..."
  />
)}

```

---

## 6\. 行选择

```tsx
import { RowSelectionState } from "@tanstack/react-table";

const [rowSelection, setRowSelection] = useState<RowSelectionState>({});

// 在列定义中添加复选框列
columnHelper.display({
  id: "select",
  header: ({ table }) => (
    <input
      type="checkbox"
      checked={table.getIsAllRowsSelected()}
      ref={(el) => {
        if (el) el.indeterminate = table.getIsSomeRowsSelected();
      }}
      onChange={table.getToggleAllRowsSelectedHandler()}
    />
  ),
  cell: ({ row }) => (
    <input
      type="checkbox"
      checked={row.getIsSelected()}
      disabled={!row.getCanSelect()}
      onChange={row.getToggleSelectedHandler()}
    />
  ),
}),

const table = useReactTable({
  data,
  columns,
  state: { rowSelection },
  onRowSelectionChange: setRowSelection,
  getCoreRowModel: getCoreRowModel(),
  enableRowSelection: true,
  // 条件性禁用选择
  // enableRowSelection: (row) => row.original.status !== "locked",
});

// 获取选中行的数据
const selectedRows = table.getSelectedRowModel().rows.map((r) => r.original);

```

---

## 7\. 列可见性

```tsx
import { VisibilityState } from "@tanstack/react-table";

const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});

const table = useReactTable({
  data,
  columns,
  state: { columnVisibility },
  onColumnVisibilityChange: setColumnVisibility,
  getCoreRowModel: getCoreRowModel(),
});

// 列可见性控制面板
<div>
  {table.getAllLeafColumns().map((column) => (
    <label key={column.id}>
      <input
        type="checkbox"
        checked={column.getIsVisible()}
        onChange={column.getToggleVisibilityHandler()}
      />
      {column.id}
    </label>
  ))}
</div>

```

---

## 8\. 服务端模式（分页 + 排序 + 过滤）

```tsx
function ServerSideTable() {
  const [sorting, setSorting] = useState<SortingState>([]);
  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
  const [pagination, setPagination] = useState<PaginationState>({
    pageIndex: 0,
    pageSize: 10,
  });

  // 同步到 TanStack Query
  const { data, isFetching } = useQuery({
    queryKey: ["users", { sorting, columnFilters, pagination }],
    queryFn: () => fetchUsers({ sorting, columnFilters, pagination }),
  });

  const table = useReactTable({
    data: data?.items ?? [],
    columns,
    rowCount: data?.total,
    state: { sorting, columnFilters, pagination },
    onSortingChange: setSorting,
    onColumnFiltersChange: setColumnFilters,
    onPaginationChange: setPagination,
    getCoreRowModel: getCoreRowModel(),
    manualSorting: true,
    manualFiltering: true,
    manualPagination: true,
  });

  return (
    <div>
      {isFetching && <span>加载中...</span>}
      {/* 渲染表格... */}
    </div>
  );
}

```

---

---

# 二、TanStack Form v1

## 1\. 基础概念

### TanStack Form 是什么

TanStack Form 是一个无头、类型安全的表单状态管理库，支持同步/异步验证，支持 React、Vue、Solid 等。

与 React Hook Form 对比：

| 特性     | TanStack Form                 | React Hook Form |
| ------ | ----------------------------- | --------------- |
| 类型安全   | 完全类型推断                        | 较好              |
| 验证适配器  | Zod / Valibot / Yup / ArkType | Zod / Yup       |
| 渲染性能   | 字段级别隔离更新                      | 受控表单            |
| SSR 支持 | 支持                            | 支持              |
| 包体积    | 较小                            | 小               |

### 安装

```bash
npm install @tanstack/react-form
# Vue
npm install @tanstack/vue-form
# Zod 适配器（可选）
npm install @tanstack/zod-form-adapter zod

```

---

## 2\. 基础使用

```tsx
import { useForm } from "@tanstack/react-form";
import { z } from "zod";
import { zodValidator } from "@tanstack/zod-form-adapter";

const userSchema = z.object({
  name: z.string().min(1, "姓名不能为空"),
  email: z.string().email("邮箱格式不正确"),
  age: z.number().int().min(18, "年龄必须大于等于 18"),
});

function CreateUserForm() {
  const form = useForm({
    defaultValues: {
      name: "",
      email: "",
      age: 18,
    },
    validatorAdapter: zodValidator(),
    validators: {
      onChange: userSchema,  // 整体 schema 验证
    },
    onSubmit: async ({ value }) => {
      await createUser(value);
    },
  });

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        form.handleSubmit();
      }}
    >
      {/* 姓名字段 */}
      <form.Field name="name">
        {(field) => (
          <div>
            <label>姓名</label>
            <input
              value={field.state.value}
              onBlur={field.handleBlur}
              onChange={(e) => field.handleChange(e.target.value)}
            />
            {field.state.meta.errors.length > 0 && (
              <p style={{ color: "red" }}>{field.state.meta.errors.join(", ")}</p>
            )}
          </div>
        )}
      </form.Field>

      {/* 邮箱字段 */}
      <form.Field name="email">
        {(field) => (
          <div>
            <label>邮箱</label>
            <input
              type="email"
              value={field.state.value}
              onBlur={field.handleBlur}
              onChange={(e) => field.handleChange(e.target.value)}
            />
            {field.state.meta.errors.map((err) => (
              <p key={err} style={{ color: "red" }}>{err}</p>
            ))}
          </div>
        )}
      </form.Field>

      <form.Subscribe selector={(s) => [s.canSubmit, s.isSubmitting]}>
        {([canSubmit, isSubmitting]) => (
          <button type="submit" disabled={!canSubmit || isSubmitting}>
            {isSubmitting ? "提交中..." : "提交"}
          </button>
        )}
      </form.Subscribe>
    </form>
  );
}

```

---

## 3\. 字段参数说明

| 属性                            | 类型         | 说明             |
| ----------------------------- | ---------- | -------------- |
| field.state.value             | T          | 字段当前值          |
| field.state.meta.errors       | string\[\] | 验证错误信息         |
| field.state.meta.isTouched    | boolean    | 字段是否被触碰过（blur） |
| field.state.meta.isDirty      | boolean    | 值是否被修改过        |
| field.state.meta.isValidating | boolean    | 异步验证进行中        |
| field.handleChange(value)     | fn         | 更新字段值          |
| field.handleBlur()            | fn         | 触发 blur 事件     |

---

## 4\. 字段级单独验证

```tsx
<form.Field
  name="username"
  validators={{
    onChange: z.string().min(3, "用户名至少 3 个字符"),
    // 异步验证（防抖 500ms）
    onChangeAsyncDebounceMs: 500,
    onChangeAsync: async ({ value }) => {
      const exists = await checkUsernameExists(value);
      if (exists) return "用户名已被使用";
      return undefined;
    },
  }}
>
  {(field) => (
    <div>
      <input
        value={field.state.value}
        onChange={(e) => field.handleChange(e.target.value)}
      />
      {field.state.meta.isValidating && <span>检查中...</span>}
      {field.state.meta.errors.map((e) => <p key={e}>{e}</p>)}
    </div>
  )}
</form.Field>

```

---

## 5\. 数组字段

```tsx
<form.Field name="tags" mode="array">
  {(field) => (
    <div>
      {field.state.value.map((_, i) => (
        <form.Field key={i} name={`tags[${i}]`}>
          {(subField) => (
            <div>
              <input
                value={subField.state.value}
                onChange={(e) => subField.handleChange(e.target.value)}
              />
              <button
                type="button"
                onClick={() => field.removeValue(i)}
              >
                删除
              </button>
            </div>
          )}
        </form.Field>
      ))}
      <button
        type="button"
        onClick={() => field.pushValue("")}
      >
        添加标签
      </button>
    </div>
  )}
</form.Field>

```

---

## 6\. 表单状态订阅

```tsx
// 订阅指定状态，避免不必要的重渲染
<form.Subscribe selector={(state) => state.values}>
  {(values) => <pre>{JSON.stringify(values, null, 2)}</pre>}
</form.Subscribe>

// 只订阅提交状态
<form.Subscribe selector={(s) => s.isSubmitting}>
  {(isSubmitting) => (
    <button disabled={isSubmitting}>
      {isSubmitting ? "提交中..." : "提交"}
    </button>
  )}
</form.Subscribe>

```

---

---

# 三、TanStack Virtual v3

## 1\. 基础概念

### TanStack Virtual 是什么

TanStack Virtual 是一个虚拟化库，用于高效渲染超长列表/表格。核心原理：只渲染可视区域内的元素，其余元素用空白占位，大幅减少 DOM 节点数量。

适用场景：

- 列表数据超过 100 条且不分页
- 下拉选项数量极多
- 与 TanStack Table 配合实现无限高性能表格

### 安装

```bash
npm install @tanstack/react-virtual
# Vue
npm install @tanstack/vue-virtual

```

---

## 2\. 基础使用

### 虚拟列表（React）

```tsx
import { useVirtualizer } from "@tanstack/react-virtual";
import { useRef } from "react";

function VirtualList({ items }: { items: string[] }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: items.length,          // 总条数
    getScrollElement: () => parentRef.current,
    estimateSize: () => 48,       // 估算每行高度（px）
    overscan: 5,                  // 可视区域外额外渲染的行数（提升滚动流畅度）
  });

  return (
    // 外层容器：固定高度，允许滚动
    <div ref={parentRef} style={{ height: "600px", overflow: "auto" }}>
      {/* 内层容器：总高度等于所有行高之和，用于占位 */}
      <div style={{ height: `${virtualizer.getTotalSize()}px`, position: "relative" }}>
        {virtualizer.getVirtualItems().map((virtualItem) => (
          <div
            key={virtualItem.key}
            style={{
              position: "absolute",
              top: 0,
              left: 0,
              width: "100%",
              height: `${virtualItem.size}px`,
              transform: `translateY(${virtualItem.start}px)`,
            }}
          >
            {items[virtualItem.index]}
          </div>
        ))}
      </div>
    </div>
  );
}

```

---

## 3\. 参数说明

| 参数               | 类型                          | 默认值      | 说明           |
| ---------------- | --------------------------- | -------- | ------------ |
| count            | number                      | 必填       | 总条目数         |
| getScrollElement | () => Element \| null       | 必填       | 滚动容器         |
| estimateSize     | (index) => number           | 必填       | 估算每项高度/宽度    |
| overscan         | number                      | 1        | 可视区外额外渲染的条目数 |
| horizontal       | boolean                     | false    | 是否水平方向虚拟化    |
| paddingStart     | number                      | 0        | 顶部填充         |
| paddingEnd       | number                      | 0        | 底部填充         |
| getItemKey       | (index) => string \| number | 使用 index | 自定义 key      |
| initialOffset    | number                      | 0        | 初始滚动偏移       |

---

## 4\. 动态高度列表

当每项高度不固定时，需要在渲染后测量实际高度：

```tsx
import { useVirtualizer } from "@tanstack/react-virtual";
import { useRef, useCallback } from "react";

function DynamicHeightList({ items }: { items: { content: string }[] }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 80,    // 估算值，实际高度会在渲染后测量
    measureElement: (element) => element?.getBoundingClientRect().height,
  });

  return (
    <div ref={parentRef} style={{ height: "600px", overflow: "auto" }}>
      <div style={{ height: `${virtualizer.getTotalSize()}px`, position: "relative" }}>
        {virtualizer.getVirtualItems().map((virtualItem) => (
          <div
            key={virtualItem.key}
            // 通过 ref 回调让 virtualizer 测量实际高度
            ref={virtualizer.measureElement}
            data-index={virtualItem.index}
            style={{
              position: "absolute",
              top: 0,
              left: 0,
              width: "100%",
              transform: `translateY(${virtualItem.start}px)`,
            }}
          >
            <p>{items[virtualItem.index].content}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

```

---

## 5\. 虚拟表格（与 TanStack Table 配合）

```tsx
import { useReactTable, getCoreRowModel } from "@tanstack/react-table";
import { useVirtualizer } from "@tanstack/react-virtual";

function VirtualTable({ data, columns }: { data: Row[]; columns: ColumnDef<Row>[] }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  });

  const rows = table.getRowModel().rows;

  const rowVirtualizer = useVirtualizer({
    count: rows.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 40,
    overscan: 10,
  });

  const virtualRows = rowVirtualizer.getVirtualItems();
  const totalSize = rowVirtualizer.getTotalSize();

  // 上下空白填充
  const paddingTop = virtualRows.length > 0 ? virtualRows[0].start : 0;
  const paddingBottom =
    virtualRows.length > 0
      ? totalSize - virtualRows[virtualRows.length - 1].end
      : 0;

  return (
    <div ref={parentRef} style={{ height: "600px", overflow: "auto" }}>
      <table style={{ width: "100%", borderCollapse: "collapse" }}>
        <thead>
          {table.getHeaderGroups().map((headerGroup) => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map((header) => (
                <th key={header.id} style={{ position: "sticky", top: 0, background: "#fff" }}>
                  {flexRender(header.column.columnDef.header, header.getContext())}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {paddingTop > 0 && <tr><td style={{ height: `${paddingTop}px` }} /></tr>}
          {virtualRows.map((virtualRow) => {
            const row = rows[virtualRow.index];
            return (
              <tr key={row.id} style={{ height: "40px" }}>
                {row.getVisibleCells().map((cell) => (
                  <td key={cell.id}>
                    {flexRender(cell.column.columnDef.cell, cell.getContext())}
                  </td>
                ))}
              </tr>
            );
          })}
          {paddingBottom > 0 && <tr><td style={{ height: `${paddingBottom}px` }} /></tr>}
        </tbody>
      </table>
    </div>
  );
}

```

---

## 6\. 水平虚拟化

```tsx
const virtualizer = useVirtualizer({
  horizontal: true,             // 水平方向
  count: columns.length,
  getScrollElement: () => parentRef.current,
  estimateSize: () => 150,      // 列宽
});

```

---

## 7\. 滚动到指定位置

```tsx
const virtualizer = useVirtualizer({ ... });

// 滚动到第 100 条
virtualizer.scrollToIndex(100);

// 平滑滚动
virtualizer.scrollToIndex(100, { behavior: "smooth" });

// 滚动到指定偏移
virtualizer.scrollToOffset(500);

```

---

## 8\. 无限滚动（与 TanStack Query 配合）

```tsx
function InfiniteList() {
  const parentRef = useRef<HTMLDivElement>(null);

  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
    queryKey: ["articles"],
    queryFn: ({ pageParam }) => fetchArticles(pageParam),
    initialPageParam: 0,
    getNextPageParam: (lastPage) => lastPage.nextCursor,
  });

  const allItems = data?.pages.flatMap((p) => p.items) ?? [];

  const virtualizer = useVirtualizer({
    count: hasNextPage ? allItems.length + 1 : allItems.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 60,
    overscan: 5,
  });

  // 当最后一项进入视图时加载更多
  useEffect(() => {
    const lastItem = virtualizer.getVirtualItems().at(-1);
    if (!lastItem) return;
    if (lastItem.index >= allItems.length - 1 && hasNextPage && !isFetchingNextPage) {
      fetchNextPage();
    }
  }, [virtualizer.getVirtualItems(), hasNextPage, allItems.length]);

  return (
    <div ref={parentRef} style={{ height: "600px", overflow: "auto" }}>
      <div style={{ height: `${virtualizer.getTotalSize()}px`, position: "relative" }}>
        {virtualizer.getVirtualItems().map((virtualItem) => {
          const isLoaderRow = virtualItem.index > allItems.length - 1;
          return (
            <div
              key={virtualItem.key}
              style={{
                position: "absolute",
                top: 0,
                left: 0,
                width: "100%",
                height: `${virtualItem.size}px`,
                transform: `translateY(${virtualItem.start}px)`,
              }}
            >
              {isLoaderRow
                ? isFetchingNextPage ? "加载中..." : "没有更多数据"
                : allItems[virtualItem.index].title}
            </div>
          );
        })}
      </div>
    </div>
  );
}

```

---

## 9\. 最佳实践

### 容器高度必须固定

Virtual 的可视区域计算依赖容器有确定的高度，不能用 `height: auto`：

```tsx
// 正确
<div style={{ height: "600px", overflow: "auto" }}>

// 正确（自适应屏幕剩余高度）
<div style={{ height: "calc(100vh - 120px)", overflow: "auto" }}>

// 错误：高度自适应，Virtual 无法工作
<div style={{ overflow: "auto" }}>

```

### estimateSize 尽量准确

估算值越接近真实高度，首次渲染的布局越准确，避免滚动条抖动。动态高度场景下配合 `measureElement` 使用。

### overscan 不宜过大

`overscan` 越大，视区外渲染的 DOM 越多，性能越差。通常 3-10 即可，滚动快的场景适当调大。

---

## 最佳实践

**Table 列定义用 `useMemo` 包裹防止无限重渲染**：`columnDefs` 数组每次渲染都是新引用，不 memo 会导致表格反复重建：

```ts
const columns = useMemo<ColumnDef<User>[]>(() => [
  { accessorKey: 'name', header: 'Name' },
  { accessorKey: 'email', header: 'Email' },
], []);

```

**服务端分页/排序/过滤时设置 `manualPagination: true`**：让 TanStack Table 不在客户端处理数据，而是通过 `onPaginationChange` 回调把分页参数传给服务端 API，与 TanStack Query 配合使用。

**Form 用 `validator` 实现字段级校验，避免整表提交才报错**：在 `fieldOptions.validators.onChange` 中实时校验，用 `field.state.meta.errors` 展示错误，让用户即时感知输入问题。

**Virtual 列表容器必须设置固定高度**：`useVirtualizer` 需要知道滚动容器的高度，使用 CSS `height: 400px` 或 `height: 100vh`，不能用 `height: auto`，否则 `getTotalSize()` 为 0。

**Virtual 动态高度用 `measureElement` 而非估算**：`estimateSize` 只是初始估算，实际渲染后调用 `measureElement` 获取真实高度，保证滚动条比例和跳转定位准确。

---

## 常见陷阱

### 陷阱：Table 排序状态与服务端数据不同步

**现象：** 点击列头排序，客户端数据按本地数组排序，但与服务端排序逻辑不一致，导致数据错乱。  
**原因：** 未设置 `manualSorting: true`，Table 默认对本地数据排序，而数据来自服务端已排序的分页结果。  
**解决：** 设置 `manualSorting: true`，监听 `onSortingChange`，将排序参数传给 API 重新请求：

```ts
const table = useReactTable({
  manualSorting: true,
  onSortingChange: setSorting,
  state: { sorting },
});

```

### 陷阱：Form `defaultValues` 异步加载后不更新

**现象：** 编辑表单中，`defaultValues` 通过 API 获取，但表单初始化时数据还未返回，之后数据到达也不更新字段。  
**原因：** TanStack Form 初始化后 `defaultValues` 不再响应外部变化，需要在数据就绪后才创建表单实例。  
**解决：** 用 TanStack Query 加载数据，`isSuccess` 为 `true` 后才渲染表单组件（或用 `key={dataVersion}` 强制重建）。

### 陷阱：Virtual 列表跳转到指定 index 时位置不准

**现象：** 调用 `scrollToIndex(100)` 后，页面滚动到大致位置，但偏差了几十像素。  
**原因：** 动态高度列表中，index 100 之前的 item 实际高度与 `estimateSize` 估算不同，累计偏差造成位置误差。  
**解决：** 滚动后用 `align: 'start'` 并等待 `measureElement` 完成，或先 `scrollToIndex` 两次（第二次确保已测量）。

---

## 参见

[TanStack Query 完全指南](https://blog.vercanti.com/tanstack-query-wan-quan-zhi-nan/)  
[TanStack Router 完全指南](https://blog.vercanti.com/tanstack-router-wan-quan-zhi-nan/)  
[TypeScript完全指南](https://blog.vercanti.com/typescript-wan-quan-zhi-nan/)  
[Vue3入门](https://blog.vercanti.com/vue-3-ru-men-zhi-nan/)