首页 / TanStack 生态入门教程 / 列固定与行选择

TanStack 生态入门教程

列固定与行选择

本教程共 38 篇 · 第 30 篇 · 更新于 2026-07-27 · 约 9 分钟阅读

TanStackTanStack 生态入门教程TanStack Table列固定行选择Row SelectionColumn Pinning列宽调整

30. 列固定与行选择

本节目标:给表格加上列固定(冻结列)、行选择(勾选行)、列宽调整(拖拽改宽)三个高级交互功能。学完你的表格能左右滚动时冻住关键列、能批量勾选行做操作、能拖拽列边框改宽度。

30.1 列固定(Column Pinning)

表格列多了,水平滚动时左边或右边的列会滚出视野。**列固定(Column Pinning)**就是把这些列「钉」在左边或右边,滚动时始终可见。

像 Excel 里冻住首行首列一个道理。

30.1.1 管理固定状态

列固定状态是个对象,有 leftright 两个数组:

import { useState } from 'react'
import type { ColumnPinningState } from '@tanstack/react-table'

const [columnPinning, setColumnPinning] = useState<ColumnPinningState>({
  left: ['name'],   // name 列固定在左侧
  right: ['actions'], // actions 列固定在右侧
})

传给表格:

const table = useReactTable({
  data,
  columns,
  getCoreRowModel: getCoreRowModel(),
  state: { columnPinning },
  onColumnPinningChange: setColumnPinning,
})
Note

列固定不需要额外的行模型。它只是个状态,影响渲染时的 CSS 定位。

30.1.2 动态固定列

在列定义里可以设置默认固定位置,也可以用 API 动态切换:

// 列定义里设默认固定
columnHelper.accessor('name', {
  header: '姓名',
  enablePinning: true, // 允许固定
})

// 动态切换某列固定位置
table.getColumn('name')?.pin('left')   // 固定到左
table.getColumn('name')?.pin('right')  // 固定到右
table.getColumn('name')?.unpin()       // 取消固定

// 检查固定状态
table.getColumn('name')?.getIsPinned() // 'left' | 'right' | false

30.1.3 渲染固定列

固定列需要 CSS 配合。核心是 position: sticky,配合 left / right 偏移量。表格实例会帮你算好偏移量:

function PinnedTable() {
  const [columnPinning, setColumnPinning] = useState<ColumnPinningState>({
    left: ['name'],
    right: ['actions'],
  })

  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
    state: { columnPinning },
    onColumnPinningChange: setColumnPinning,
  })

  return (
    <div style={{ overflowX: 'auto', maxWidth: 600 }}>
      <table style={{ borderCollapse: 'collapse' }}>
        <thead>
          {table.getHeaderGroups().map((headerGroup) => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map((header) => {
                const isPinned = header.column.getIsPinned()
                const pinStyles = isPinned
                  ? {
                      position: 'sticky' as const,
                      left: header.getStart('left'),   // 左固定列的 left 偏移
                      right: header.getStart('right'), // 右固定列的 right 偏移
                      zIndex: 2,
                      background: '#f9fafb',
                    }
                  : {}

                return (
                  <th
                    key={header.id}
                    style={{
                      border: '1px solid #d1d5db',
                      padding: '8px',
                      ...pinStyles,
                    }}
                  >
                    {flexRender(header.column.columnDef.header, header.getContext())}
                  </th>
                )
              })}
            </tr>
          ))}
        </thead>
        <tbody>
          {table.getRowModel().rows.map((row) => (
            <tr key={row.id}>
              {row.getVisibleCells().map((cell) => {
                const isPinned = cell.column.getIsPinned()
                const pinStyles = isPinned
                  ? {
                      position: 'sticky' as const,
                      left: cell.getStart('left'),
                      right: cell.getStart('right'),
                      zIndex: 1,
                      background: '#ffffff',
                    }
                  : {}

                return (
                  <td
                    key={cell.id}
                    style={{ border: '1px solid #d1d5db', padding: '8px', ...pinStyles }}
                  >
                    {flexRender(cell.column.columnDef.cell, cell.getContext())}
                  </td>
                )
              })}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  )
}

关键点:

  1. 外层容器设 overflowX: 'auto',让表格能水平滚动。
  2. 固定列的 th/tdposition: stickyleft/rightgetStart() 拿到。
  3. 固定列要设背景色,不然滚动时后面的内容会透过来。
  4. 表头的 zIndex 要比表体高,防止滚动时表头被覆盖。
Warning

getStart('left') 返回的是该列相对于左侧固定区域的偏移量。如果有多个左固定列,第二个列的 left 会自动叠加第一个列的宽度。这个计算由表格实例完成,你只需要拿值用。

30.2 行选择(Row Selection)

行选择就是让用户勾选某些行,常用于批量操作—批量删除、批量导出等。

30.2.1 开启行选择

行选择不需要额外的行模型,只需要管理状态和提供判断函数:

import { useState } from 'react'
import type { RowSelectionState } from '@tanstack/react-table'

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

const table = useReactTable({
  data,
  columns,
  getCoreRowModel: getCoreRowModel(),
  enableRowSelection: true,        // 允许行选择
  onRowSelectionChange: setRowSelection,
  state: { rowSelection },
})

enableRowSelection 可以是布尔值也可以是函数。函数形式能按行控制是否可选:

// 只有 status 为 active 的行可选
enableRowSelection: (row) => row.original.status === 'active'

30.2.2 行选择状态

rowSelection 是个对象,key 是行 id,value 是 true

// rowSelection 的结构
{
  '0': true,  // 第 0 行被选中
  '2': true,  // 第 2 行被选中
}

行 id 默认是行索引。想用数据的 id 字段当行 id,设 getRowId

const table = useReactTable({
  data,
  columns,
  getCoreRowModel: getCoreRowModel(),
  getRowId: (row) => row.id, // 用数据的 id 字段当行 id
  enableRowSelection: true,
  state: { rowSelection },
  onRowSelectionChange: setRowSelection,
})
Tip

强烈建议设 getRowId。默认用索引当 id,数据排序或过滤后索引变了,选中状态会错乱。用数据唯一 id 当行 id,选中状态才稳定。

30.2.3 渲染选择框

在列定义里加一列复选框:

const columns = [
  columnHelper.display({
    id: 'select',
    header: ({ table }) => (
      <input
        type="checkbox"
        checked={table.getIsAllRowsSelected()}
        indeterminate={table.getIsSomeRowsSelected()}
        onChange={table.getToggleAllRowsSelectedHandler()}
      />
    ),
    cell: ({ row }) => (
      <input
        type="checkbox"
        checked={row.getIsSelected()}
        onChange={row.getToggleSelectedHandler()}
        disabled={!row.getCanSelect()}
      />
    ),
  }),
  // ... 其他列
]

关键方法:

  • table.getIsAllRowsSelected() — 是否全选。
  • table.getIsSomeRowsSelected() — 是否部分选中(用于 indeterminate 半选状态)。
  • table.getToggleAllRowsSelectedHandler() — 全选/取消全选的处理器。
  • row.getIsSelected() — 这行是否选中。
  • row.getToggleSelectedHandler() — 切换这行选中的处理器。
  • row.getCanSelect() — 这行能不能选。
Note

indeterminate 属性控制复选框的半选状态(一个横杠)。当部分行被选中但不是全部时,表头复选框应显示半选状态。原生 HTML 的 indeterminate 只能通过 JS 设置,不能通过属性写死。

30.2.4 获取选中的行

// 获取选中的行对象数组
const selectedRows = table.getSelectedRowModel().rows

// 获取选中行的原始数据
const selectedData = table.getSelectedRowModel().rows.map((row) => row.original)

// 获取选中行的 id 数组
const selectedIds = Object.keys(rowSelection)

拿到选中数据后,就能做批量操作:

function handleBatchDelete() {
  const selectedData = table.getSelectedRowModel().rows.map((row) => row.original)
  if (selectedData.length === 0) {
    alert('请先选择要删除的行')
    return
  }
  deleteRows(selectedData.map((row) => row.id))
  // 清除选中状态
  table.resetRowSelection()
}

30.3 列宽调整(Column Resizing)

用户可能想拖拽列边框来调整列宽。TanStack Table 内置了这个能力。

30.3.1 开启列宽调整

const [columnSizing, setColumnSizing] = useState<ColumnSizingState>({})

const table = useReactTable({
  data,
  columns,
  getCoreRowModel: getCoreRowModel(),
  enableColumnResizing: true, // 开启列宽调整
  columnResizeMode: 'onChange', // 拖拽时实时更新
  state: { columnSizing },
  onColumnSizingChange: setColumnSizing,
})

columnResizeMode 有两种:

  • 'onChange' — 拖拽过程中实时更新列宽,体验流畅但性能开销大。
  • 'onEnd' — 拖拽结束时才更新,性能好但拖拽过程中看不到效果。

30.3.2 列定义里设列宽

const columns = [
  columnHelper.accessor('name', {
    header: '姓名',
    size: 150,    // 建议宽度
    minSize: 80,  // 最小宽度
    maxSize: 300, // 最大宽度
  }),
  columnHelper.accessor('age', {
    header: '年龄',
    size: 100,
    enableResizing: false, // 禁止这列调整宽度
  }),
]

30.3.3 渲染拖拽手柄

在表头里加个拖拽手柄:

<thead>
  {table.getHeaderGroups().map((headerGroup) => (
    <tr key={headerGroup.id}>
      {headerGroup.headers.map((header) => (
        <th
          key={header.id}
          style={{ width: header.getSize(), position: 'relative' }}
        >
          {flexRender(header.column.columnDef.header, header.getContext())}

          {/* 拖拽手柄 */}
          {header.column.getCanResize() && (
            <div
              onMouseDown={header.getResizeHandler()}
              onTouchStart={header.getResizeHandler()}
              style={{
                position: 'absolute',
                right: 0,
                top: 0,
                height: '100%',
                width: '4px',
                background: header.column.getIsResizing() ? '#3b82f6' : '#d1d5db',
                cursor: 'col-resize',
                userSelect: 'none',
              }}
            />
          )}
        </th>
      ))}
    </tr>
  ))}
</thead>

关键点:

  • header.getSize() 拿当前列宽,设到 thwidth 上。
  • header.getResizeHandler() 返回拖拽处理器,绑到 onMouseDownonTouchStart
  • header.column.getIsResizing() 判断是否正在拖拽,可改手柄颜色给反馈。
  • 表体单元格也要设 width: cell.column.getSize() 保持宽度和表头一致。
Tip

列宽调整时要给 <table>tableLayout: 'fixed',否则浏览器可能忽略你设的宽度,按内容自动分配。

30.4 行固定(Row Pinning)

和列固定类似,行也能固定在顶部或底部。常用于固定首行或汇总行。

const [rowPinning, setRowPinning] = useState<RowPinningState>({
  top: [],    // 固定在顶部的行 id
  bottom: [], // 固定在底部的行 id
})

const table = useReactTable({
  data,
  columns,
  getCoreRowModel: getCoreRowModel(),
  getRowId: (row) => row.id,
  state: { rowPinning },
  onRowPinningChange: setRowPinning,
  enableRowPinning: true,
})

// 固定某行到顶部
table.getRow('1')?.pin('top')
// 固定某行到底部
table.getRow('5')?.pin('bottom')
// 取消固定
table.getRow('1')?.unpin()

渲染时,固定行的 trposition: sticky + top/bottom 偏移,和列固定的 CSS 套路一样。

30.5 常见坑

坑一:固定列没有背景色。 忘了给固定列设背景色,滚动时后面的内容透过来,字叠在一起看不清。表头和表体的固定列都要设背景色,且表头 z-index 更高。

坑二:行选择用索引当 id,排序后选中错乱。 默认行 id 是索引,排序后索引变了,选中状态跟着错位。设 getRowId: (row) => row.id 用数据 id 当行 id。

坑三:全选只选当前页。 分页 + 行选择时,getToggleAllRowsSelectedHandler 选的是所有行(包括其他页)。如果想只选当前页,用 table.getToggleAllPageRowsSelectedHandler()

坑四:列宽拖拽没效果。 没设 enableColumnResizing: true,或者 <table> 没设 tableLayout: 'fixed'。后者更常见—浏览器默认 tableLayout: auto,会按内容分配宽度,忽略你设的 width

坑五:indeterminate 状态不生效。 React 不支持直接写 indeterminate 属性。需要用 ref 在 DOM 上设置:ref={(el) => el && (el.indeterminate = table.getIsSomeRowsSelected())}

30.6 小结

这一章讲了三个高级交互:

  • 列固定(Column Pinning):管理 columnPinning 状态,渲染时用 position: sticky + getStart() 偏移量。固定列必须设背景色和 z-index。
  • 行选择(Row Selection):管理 rowSelection 状态,用 getRowId 设稳定行 id,表头用全选处理器,表体用行选择处理器。getSelectedRowModel() 拿选中数据。
  • 列宽调整(Column Resizing):开 enableColumnResizing,列定义设 size/minSize/maxSize,表头加拖拽手柄,<table>tableLayout: 'fixed'
  • 行固定(Row Pinning):和列固定类似,position: sticky 钉在顶部或底部。

下一章是 Table 部分的收尾,讲表格状态管理和与 Virtual 集成做大数据虚拟化。