列固定与行选择
本教程共 38 篇 · 第 30 篇 · 更新于 2026-07-27 · 约 9 分钟阅读
30. 列固定与行选择
本节目标:给表格加上列固定(冻结列)、行选择(勾选行)、列宽调整(拖拽改宽)三个高级交互功能。学完你的表格能左右滚动时冻住关键列、能批量勾选行做操作、能拖拽列边框改宽度。
30.1 列固定(Column Pinning)
表格列多了,水平滚动时左边或右边的列会滚出视野。**列固定(Column Pinning)**就是把这些列「钉」在左边或右边,滚动时始终可见。
像 Excel 里冻住首行首列一个道理。
30.1.1 管理固定状态
列固定状态是个对象,有 left 和 right 两个数组:
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>
)
}
关键点:
- 外层容器设
overflowX: 'auto',让表格能水平滚动。 - 固定列的
th/td设position: sticky,left/right用getStart()拿到。 - 固定列要设背景色,不然滚动时后面的内容会透过来。
- 表头的
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()拿当前列宽,设到th的width上。header.getResizeHandler()返回拖拽处理器,绑到onMouseDown和onTouchStart。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()
渲染时,固定行的 tr 设 position: 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 集成做大数据虚拟化。