TanStack Table 列可见性(Column Visibility)表级选项深度解析:enableHiding 与 onColumnVisibilityChange 完整指南

原创2026-09-21 23:55:171,693 阅读
文章标签:前端UI组件

TanStack Table 列可见性(Column Visibility)表级选项深度解析:enableHiding 与 onColumnVisibilityChange 完整指南

本篇技术指南聚焦 TanStack Table(table-core 核心 + React/Vue/Solid/Svelte 等框架适配层)中列可见性(Column Visibility)特性的两个表级配置选项——enableHiding 与 onColumnVisibilityChange。它们定义于 TableOptions_ColumnVisibility 接口,是启用列隐藏/显示能力、接入受控状态(controlled state)的入口。读完本文,你将掌握这两个选项的默认行为、与 columnVisibility 状态的关系、外部 Atom 与回调两种状态管理模式,并能结合源码与示例搭建一个完整可用的"显示/隐藏列"控制面板。

接口概览:TableOptions_ColumnVisibility

该接口在仓库中的定义位于 columnVisibilityFeature.types.ts,完整签名如下:

export interface TableOptions_ColumnVisibility {
  /**
   * Whether to enable column hiding. Defaults to `true`.
   */
  enableHiding?: boolean
  /**
   * Called with an updater when column visibility state changes. Pair this with
   * `state.columnVisibility` when using external state; external atoms can own
   * the slice without this callback.
   */
  onColumnVisibilityChange?: OnChangeFn<ColumnVisibilityState>
}

接口仅包含两个可选属性,集中体现了列可见性特性的全部表级配置面:

属性 类型 默认值 作用
enableHiding boolean(可选) true 是否允许隐藏列(表级总开关)
onColumnVisibilityChange OnChangeFn<ColumnVisibilityState>(可选) 由 makeStateUpdater 生成的内部更新器 列可见性状态变化时被调用的回调,配合 state.columnVisibility 实现受控模式

配套的状态类型 ColumnVisibilityState 定义在同一文件顶部(columnVisibilityFeature.types.ts):

export type ColumnVisibilityState = Record<string, boolean>

即一个"列 ID → 布尔值"的映射:某列 ID 的值为 false 表示该列被隐藏;值为 true 或该列 ID 不在映射中都表示该列可见。这一"缺失即可见"的语义是整个特性的核心约定,下文源码分析会反复印证。

选项一:enableHiding——控制列能否被隐藏

enableHiding 是列隐藏能力的总开关,默认值为 true,即默认情况下所有列都可以被隐藏或重新显示。当你希望某些列(例如主键、操作按钮列)始终固定可见时,有两种层级可以关闭隐藏能力。

表级全局禁用

将 enableHiding: false 传入 useTable 的选项对象,即可让所有列都无法隐藏:

const table = useTable({
  features,
  columns,
  data,
  enableHiding: false, // 全局禁止隐藏任何列
})

列级定向禁用

更常见的做法是在**列定义(ColumnDef)**上使用列级 enableHiding,只锁定特定列。列级选项定义于 ColumnDef_ColumnVisibility(源码见 columnVisibilityFeature.types.ts):

const columns = [
  {
    header: 'ID',
    accessorKey: 'id',
    enableHiding: false, // 该列禁止隐藏
  },
  {
    header: 'Name',
    accessorKey: 'name', // 未设置,默认可隐藏
  },
]

组合判定逻辑:column_getCanHide

列级与表级两个开关并非二选一,而是**"与"关系**。核心实现位于 columnVisibilityFeature.utils.ts:

export function column_getCanHide(column) {
  return (
    (column.columnDef.enableHiding ?? true) &&
    (column.table.options.enableHiding ?? true)
  )
}

即:只有当列定义的 enableHiding(缺省视为 true)且表级 enableHiding(缺省视为 true)都为真时,该列才允许被隐藏。任一设置为 false,column.getCanHide() 都会返回 false。测试用例 columnVisibilityFeature.utils.test.ts 分别验证了"全局禁用"与"列级禁用"两条路径,均使 column_getCanHide 返回 false。

提示:即使某列被锁定为不可隐藏,它的 ID 仍可能出现在 columnVisibility 状态映射中,但 column_toggleVisibility 在调用前会先检查 column_getCanHide,不可隐藏的列会被直接跳过,状态不会被修改(见 utils 实现 columnVisibilityFeature.utils.ts)。

选项二:onColumnVisibilityChange——受控状态的更新回调

onColumnVisibilityChange 的完整类型为 OnChangeFn<ColumnVisibilityState>,即"接收一个更新器(updater)的函数"。更新器可以是新的状态映射,也可以是接收旧状态并返回新状态的函数:

// 直接传入新映射
onColumnVisibilityChange({ visits: false })

// 或传入函数式更新器
onColumnVisibilityChange((old) => ({ ...old, visits: false }))

该回调在以下场景被触发:

  • table.setColumnVisibility(updater)
  • table.toggleAllColumnsVisible(value)
  • 任一列执行 column.toggleVisibility(value)
  • table.resetColumnVisibility(defaultState)

默认行为:内部状态更新器

即使你不传该选项,列可见性依然可用。feature 在注册时通过 getDefaultTableOptions 为 onColumnVisibilityChange 提供了默认实现,见 columnVisibilityFeature.ts:

getDefaultTableOptions: (table) => {
  return {
    onColumnVisibilityChange: makeStateUpdater('columnVisibility', table),
  }
}

makeStateUpdater('columnVisibility', table) 生成一个内部更新器,负责将新状态写回表的 state.columnVisibility 并触发相关订阅。这解释了为什么"什么都不配"也能开箱即用地隐藏/显示列——默认情况下表格自己管理这份状态。

受控模式:state.columnVisibility + onColumnVisibilityChange

当你需要自行拥有 columnVisibility 状态(例如持久化用户偏好、与表单联动、在表格外部读写),v8 风格的受控模式仍然受支持:把状态值传给 state.columnVisibility,把 setter 传给 onColumnVisibilityChange:

const [columnVisibility, setColumnVisibility] = useState<ColumnVisibilityState>({
  columnId1: true,
  columnId2: false, // 默认隐藏该列
  columnId3: true,
})

const table = useTable({
  features,
  columns,
  data,
  state: {
    columnVisibility,
  },
  onColumnVisibilityChange: setColumnVisibility,
})

推荐方案:外部 Atom(无需该回调)

按原文档的说明(React 指南),外部 Atom 可以直接拥有这个状态切片,从而无需 onColumnVisibilityChange 回调。外部 Atom 提供全应用范围内的细粒度订阅,表格之外的其他代码也能读写可见性状态,而无需重新渲染拥有表格的组件:

import { useCreateAtom, useSelector } from '@tanstack/react-store'
import { columnVisibilityFeature, tableFeatures, useTable } from '@tanstack/react-table'
import type { ColumnVisibilityState } from '@tanstack/react-table'

const features = tableFeatures({ columnVisibilityFeature })

const columnVisibilityAtom = useCreateAtom<ColumnVisibilityState>({
  columnId1: true,
  columnId2: false, // 默认隐藏该列
  columnId3: true,
})

const columnVisibility = useSelector(columnVisibilityAtom) // 在任意需要的地方订阅

const table = useTable({
  features,
  columns,
  data,
  atoms: {
    columnVisibility: columnVisibilityAtom,
  },
})

此时可见性状态的写入经由 atoms.columnVisibility 直接落到外部 Atom,onColumnVisibilityChange 不再是必需项。这是 v9 框架适配层推荐的状态管理方式。

非受控模式:initialState 设定初始值

如果状态完全交给表格内部管理,只需用 initialState.columnVisibility 指定初始可见性:

const table = useTable({
  features,
  columns,
  data,
  initialState: {
    columnVisibility: {
      columnId1: true,
      columnId2: false, // 首屏默认隐藏该列
      columnId3: true,
    },
  },
})

注意:若 columnVisibility 同时出现在 initialState 和 state 中,state 的初始化优先,initialState 会被忽略。二者只能二选一(见 React 指南 的 NOTE 提示)。

状态语义与默认状态

列可见性 feature 在注册时通过 getInitialState 注入初始状态(columnVisibilityFeature.ts),默认状态由 getDefaultColumnVisibilityState() 生成,即一个空对象 {}(实现见 columnVisibilityFeature.utils.ts)。

空对象的语义是:所有列 ID 在映射中缺失,因此所有列默认可见。测试 columnVisibilityFeature.utils.test.ts 验证了 getDefaultColumnVisibilityState() 返回 {},且 column_getIsVisible 在默认情况下返回 true。

column_getIsVisible 的判定逻辑(columnVisibilityFeature.utils.ts)还处理了两种特殊情况:

  • 叶子列(leaf):读取 state.columnVisibility[column.id],缺失时回退为 true;
  • 父/分组列(parent/group):自身没有直接状态条目,而是递归判断任一子列可见则该父列可见。测试用例"should return true if any child column is visible"验证了这一点(columnVisibilityFeature.utils.test.ts)。

配套 API 家族:Table、Column、Row 三层

TableOptions_ColumnVisibility 只是配置入口,特性启用后会为三个对象注入完整 API(类型定义见 columnVisibilityFeature.types.ts,注册逻辑见 columnVisibilityFeature.ts)。

Table 级 API

API 说明
getIsAllColumnsVisible() 是否所有叶子列都可见(用于"全选"复选框的 checked)
getIsSomeColumnsVisible() 是否至少一个叶子列可见(用于三态控制)
getToggleAllColumnsVisibilityHandler() 复选框风格处理器,读取 event.target.checked 并切换全部列
getVisibleFlatColumns() 当前可见的扁平列列表(含仍有可见后代的父列)
getVisibleLeafColumns() 当前可见的叶子列列表(行单元格与表头渲染通常用它)
resetColumnVisibility(defaultState?) 重置为 initialState.columnVisibility;传 true 则忽略初始状态、重置为 {}
setColumnVisibility(updater) 以新映射或更新器函数更新可见性状态
toggleAllColumnsVisible(value?) 显示/隐藏所有可隐藏的叶子列

getToggleAllColumnsVisibilityHandler 的源码(columnVisibilityFeature.utils.ts)展示了它与 onColumnVisibilityChange 的联动:处理器把 event.target.checked 传给 table_toggleAllColumnsVisible,后者构建完整映射并调用 table_setColumnVisibility,最终经由 setStateSlice 路由到 onColumnVisibilityChange(默认是内部更新器,受控模式下是你传入的 setter)。

Column 级 API

API 说明
getCanHide() 该列是否允许被隐藏(综合列级与表级 enableHiding)
getIsVisible() 该列当前是否可见
getToggleVisibilityHandler() 复选框风格处理器,读取 event.target.checked 切换该列
toggleVisibility(value?) 切换该列可见性;不传值时自动取反

完整签名见 Column_ColumnVisibility。

Row 级 API

API 说明
getVisibleCells() 该行中属于可见列的单元格(启用列固定时按 start → center → end 排序)
getVisibleCellsByColumnId() 以列 ID 为键的可见单元格映射,隐藏列被剔除

渲染要点:务必使用"可见"系列 API

启用列隐藏后,渲染表头、表体与表脚时不要使用 table.getAllLeafColumns()、row.getAllCells() 这类忽略可见性的 API,而要改用 table.getVisibleLeafColumns()、row.getVisibleCells(),否则被隐藏的列仍会渲染出来。表头分组 API(table.getHeaderGroups() 等)本身已经考虑了列可见性,可放心使用(见 React 指南)。

table_getVisibleLeafColumns 与 row_getVisibleCells 的实现(columnVisibilityFeature.utils.ts)都是先取全集、再按 column_getIsVisible 过滤;row_getVisibleCells 额外处理了列固定(pinning)下的排序。两个 API 在 columnVisibilityFeature.ts 中注册了 memo 依赖(含 columnVisibility、columnOrder、columnPinning、grouping 等状态),状态变化时自动失效重算。

端到端示例:构建"显示/隐藏列"控制面板

仓库的官方示例 examples/react/column-visibility/src/main.tsx 是上述选项与 API 的完整落地,核心结构如下:

const features = tableFeatures({ columnVisibilityFeature })

const table = useTable({
  features,
  columns,
  data,
  // initialState: { columnVisibility: { visits: false } }, // 首屏隐藏某列
  // atoms: { columnVisibility: columnVisibilityAtom },       // 推荐:外部 Atom 拥有状态
  // state: { columnVisibility },                             // 受控模式
  // onColumnVisibilityChange: setColumnVisibility,
  // enableHiding: false,                                     // 全局禁止隐藏
  debugTable: true,
})

// 全选开关 + 逐列开关
<table>
  <thead>
    {table.getHeaderGroups().map((headerGroup) => (
      <tr key={headerGroup.id}>
        {headerGroup.headers.map((header) => (
          <th key={header.id} colSpan={header.colSpan}>
            {header.isPlaceholder ? null : <table.FlexRender header={header} />}
          </th>
        ))}
      </tr>
    ))}
  </thead>
  <tbody>
    {table.getRowModel().rows.map((row) => (
      <tr key={row.id}>
        {row.getVisibleCells().map((cell) => (
          <td key={cell.id}>
            <table.FlexRender cell={cell} />
          </td>
        ))}
      </tr>
    ))}
  </tbody>
</table>

控制面板部分将"全选"复选框绑定到 table.getIsAllColumnsVisible() 与 table.getToggleAllColumnsVisibilityHandler(),将逐列复选框绑定到 column.getIsVisible() 与 column.getToggleVisibilityHandler(),与 React 指南 中给出的模板一致。该示例同时提供了 e2e 冒烟测试 examples/react/column-visibility/tests/e2e/smoke.spec.ts,用于验证切换列可见性后的渲染结果。

提示:可见性菜单通常渲染列本身而非表头对象——被隐藏的列可能没有活动的表头上下文。建议使用稳定的文本标签(如自定义的"列 ID → 标签"映射、columnDef.meta 中的 label 字段,或直接使用 column.id)作为开关文案(见 React 指南 的 NOTE)。

源码链路小结

将以上内容串起来,一次"隐藏某列"操作背后的完整调用链为:

  1. UI 触发 column.getToggleVisibilityHandler() 或 column.toggleVisibility(false);
  2. column_toggleVisibility 校验 column_getCanHide(列级与表级 enableHiding 与运算),并将新的可见性写入各叶子列(columnVisibilityFeature.utils.ts);
  3. table_setColumnVisibility 通过 setStateSlice 将更新器路由到 onColumnVisibilityChange(columnVisibilityFeature.utils.ts);
  4. 默认情况下该回调是 makeStateUpdater 生成的内部更新器,将状态写回表内;受控模式下则由你的 onColumnVisibilityChange 接收更新器并驱动 state.columnVisibility;外部 Atom 模式下状态直接落到 Atom;
  5. 状态变化使 getVisibleLeafColumns、getVisibleCells 等 memo 依赖失效,重算后触发渲染,隐藏的列从表头与表体中消失。

常见误区

  • 使用全集 API 渲染:getAllLeafColumns() / getAllCells() 不感知可见性,渲染时必须改用 getVisibleLeafColumns() / getVisibleCells();
  • initialState 与 state 同时提供 columnVisibility:state 优先,initialState 被忽略,请二选一;
  • 误以为 enableHiding: false 会从状态中清除该列:不可隐藏的列只是不会被 toggleVisibility 修改,若它已在状态映射中,仍可通过 setColumnVisibility 显式赋值(虽然不推荐);
  • 将分组列 ID 直接写入状态:可见性状态按叶子列 ID 索引,对分组列调用 toggleVisibility 会扩散到其可隐藏的叶子列,而非写入分组列自身 ID(有测试用例专门验证,见 columnVisibilityFeature.utils.test.ts)。

延伸阅读

登录后查看全文
table