TanStack Table / Form / Virtual 完全指南

最后更新:2026-03-29 TanStack Table 是一个无头(Headless)表格库,只提供逻辑和状态管理,不提供任何 UI 样式,完全由开发者控制渲染。支持 React、Vue、Solid、Svelte。 无头的含义:Table 的核心是一个状态管理器,你传入数据和列定义,它返回处理好的行、列、单元格数据和操作方法,具体如何渲染成 HTML 完全自定义。 TanStack Form 是一个无头、类型安全的表单状态管理库,支持同步/异步验证,支持 React、Vue、Solid 等。 与 React Hook Form 对比: TanSta

分享

最后更新: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 完全自定义。

安装

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)

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. 排序

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. 分页

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. 过滤

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. 行选择

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. 列可见性

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. 服务端模式(分页 + 排序 + 过滤)

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 支持 支持 支持
包体积 较小

安装

npm install @tanstack/react-form
# Vue
npm install @tanstack/vue-form
# Zod 适配器(可选)
npm install @tanstack/zod-form-adapter zod

2. 基础使用

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. 字段级单独验证

<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. 数组字段

<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. 表单状态订阅

// 订阅指定状态,避免不必要的重渲染
<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 配合实现无限高性能表格

安装

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

2. 基础使用

虚拟列表(React)

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. 动态高度列表

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

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 配合)

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. 水平虚拟化

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

7. 滚动到指定位置

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

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

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

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

8. 无限滚动(与 TanStack Query 配合)

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

// 正确
<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 会导致表格反复重建:

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: 400pxheight: 100vh,不能用 height: auto,否则 getTotalSize() 为 0。

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


常见陷阱

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

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

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

陷阱:Form defaultValues 异步加载后不更新

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

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

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


参见

TanStack Query 完全指南
TanStack Router 完全指南
TypeScript完全指南
Vue3入门

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog