TanStack Table React 的 SubscribeProps 类型别名:用三路联合类型实现细粒度表状态订阅
2026/9/21 1:54:14 网站建设 项目流程

TanStack Table React 的 SubscribeProps 类型别名:用三路联合类型实现细粒度表状态订阅

【免费下载链接】table🤖 Headless UI for building powerful tables & datagrids for TS/JS - React-Table, Vue-Table, Solid-Table, Svelte-Table项目地址: https://gitcode.com/gh_mirrors/ta/table

导读

在 TanStack Table v9(本仓库对应packages/react-table)中,状态管理全面转向 TanStack Store 的原子化模型,而SubscribeProps正是驱动这一模型的核心类型契约。它定义了Subscribe组件与table.Subscribe方法接受的全部 props 形态:既可以订阅整个table.store,也可以只订阅某个状态原子(如table.atoms.rowSelection),并且通过选择器(selector)投影出最小化的渲染数据。读完本文,你将完整掌握SubscribeProps的三个联合分支、SubscribeSource的取值来源,以及如何在表格中落地"只在需要的地方重渲染"的精细订阅方案。

SubscribeProps 的类型签名与泛型参数

SubscribeProps定义在 react-table/src/Subscribe.ts,是三种订阅形态的联合类型(union type):

export type SubscribeProps< TFeatures extends TableFeatures, TSelected = unknown, TSourceValue = unknown, > = | SubscribePropsWithStore<TFeatures, TSelected> | SubscribePropsWithSourceIdentity<TSourceValue> | SubscribePropsWithSourceWithSelector<TSourceValue, TSelected>

三个泛型参数各有明确职责:

泛型参数约束 / 默认值含义
TFeaturesextends TableFeatures表特性集合类型。由tableFeatures({...})注册的特性决定哪些状态切片可用(例如注册了rowSelectionFeature才有table.atoms.rowSelection
TSelected默认unknown选择器投影后的值类型,即children渲染函数实际接收的参数类型
TSourceValue默认unknown订阅源(atom 或 store)的值类型,例如RowSelectionState

这里采用联合类型而非单一对象类型,是为了在类型层面强制约束三种互斥的订阅模式:store 模式必须显式提供 selector,source 模式要么不提供 selector(恒等投影),要么提供 selector(投影子集)。这样 TypeScript 可以在编译期拦截"订阅了整个 store 却没有投影"这类容易引发大面积重渲染的误用。

分支一:SubscribePropsWithStore —— 订阅完整表状态(强制 selector)

SubscribePropsWithStore定义在 react-table/src/Subscribe.ts,对应文档中"Subscribe totable.store(full table state)"的语义,其结构为:

export type SubscribePropsWithStore< TFeatures extends TableFeatures, TSelected, > = { source: SubscribeSource<TableState<TFeatures>> selector: (state: TableState<TFeatures>) => TSelected children: ((state: TSelected) => ReactNode) | ReactNode }

该分支的三个属性要点:

  • source:类型为SubscribeSource<TableState<TFeatures>>,即订阅源是承载完整TableState的 store(通常是table.store)。
  • selector必填。接收完整的TableState,返回投影值TSelected。源码注释明确说明:"Required in store mode so you never accidentally subscribe to the whole store without an explicit projection"——在 store 模式下 selector 是强制的,防止你无意中订阅整个 store 而不做任何投影。同时注释指出其比较策略为浅比较(shallow compare):"Re-renders when the selected value changes (shallow compare)"。
  • children:可以是接收TSelected的渲染函数(render prop),也可以是一个静态ReactNode

典型用法是同时投影多个状态切片,聚合到一个渲染块中:

<Subscribe source={table.store} selector={(state) => ({ columnFilters: state.columnFilters, globalFilter: state.globalFilter, rowSelection: state.rowSelection, })} > {() => ( <IndeterminateCheckbox checked={table.getIsAllRowsSelected()} indeterminate={table.getIsSomeRowsSelected()} onChange={table.getToggleAllRowsSelectedHandler()} /> )} </Subscribe>

该示例取自 basic-subscribe 示例:表头全选复选框依赖过滤与行选择三类状态,因此用 store 模式一次性投影。

分支二:SubscribePropsWithSourceIdentity —— 订阅源的完整值(省略 selector)

SubscribePropsWithSourceIdentity定义在 react-table/src/Subscribe.ts,对应文档中"Subscribe to the full value of a source (e.g.table.atoms.rowSelectionortable.optionsStore)"的语义:

export type SubscribePropsWithSourceIdentity<TSourceValue> = { source: SubscribeSource<TSourceValue> selector?: undefined children: ((state: TSourceValue) => ReactNode) | ReactNode }

关键语义:

  • selector是可选属性,且类型被限定为undefined。源码注释解释:"Omittingselectoris equivalent to the identity selector — children receiveTSourceValue"。也就是说省略 selector 等价于恒等选择器,children直接收到订阅源的完整值。
  • 之所以把selector声明为optional selector: undefined,而不是直接不声明该属性,是为了让联合类型的分支判别更精确——TypeScript 可以据此区分"恒等投影"与"带 selector 的投影"两种形态,从而获得更好的类型推断。
  • children是渲染函数时,直接接收TSourceValue

最典型的场景是只关心某一个状态切片(atom):

// 订阅行选择原子,children 直接拿到完整的 RowSelectionState <table.Subscribe source={table.atoms.rowSelection}> {(rowSelection) => ( <div>Selected rows: {Object.keys(rowSelection).length}</div> )} </table.Subscribe>

分支三:SubscribePropsWithSourceWithSelector —— 对订阅源做投影

SubscribePropsWithSourceWithSelector定义在 react-table/src/Subscribe.ts,对应文档中"Subscribe to a projected value from a source (atom or store)"的语义:

export type SubscribePropsWithSourceWithSelector<TSourceValue, TSelected> = { source: SubscribeSource<TSourceValue> selector: (state: TSourceValue) => TSelected children: ((state: TSelected) => ReactNode) | ReactNode }

与恒等分支的差别在于:

  • selector为必填,接收源值TSourceValue,返回投影后的TSelected
  • children渲染函数收到的不是源值,而是投影值TSelected

这是把订阅范围压缩到"最小渲染面"的核心手段。例如行选择场景中,整张表只关心当前这一行是否被选中,而非整个选择状态:

<table.Subscribe source={table.atoms.rowSelection} selector={(rowSelection) => rowSelection[row.id]} > {(isRowSelected) => ( <div className="column-toggle-row"> <IndeterminateCheckbox checked={!!isRowSelected} disabled={!row.getCanSelect()} indeterminate={row.getIsSomeSelected()} onChange={row.getToggleSelectedHandler()} /> </div> )} </table.Subscribe>

该写法来自 basic-subscribe 示例:selector把整份RowSelectionState投影成当前行的布尔值,因此切换某一行时,只有该行复选框所在的订阅岛会重渲染,其他行不受影响。

SubscribeSource:订阅源的统一抽象

三种分支共享同一个source属性,其类型SubscribeSource定义在同文件的 react-table/src/Subscribe.ts:

export type SubscribeSource<TValue> = | Atom<TValue> | ReadonlyAtom<TValue> | Store<TValue> | ReadonlyStore<TValue>

它囊括了 TanStack Store 的四种可订阅对象,即@tanstack/react-store导出的AtomReadonlyAtomStoreReadonlyStore。这一抽象使得Subscribe既可以订阅单一状态切片(atom),也可以订阅完整的扁平化状态仓库(store),且不区分读写形态——只要具备可订阅协议即可。在 TanStack Table v9 的状态模型中:

  • table.baseAtoms是由初始状态解析出的内部可写原子;
  • table.atoms是按注册状态切片暴露的只读派生原子(如table.atoms.rowSelectiontable.atoms.pagination);
  • table.store是把所有已注册的table.atoms聚合而成的只读扁平化 Store。

(详见 Table State 指南。)正因为 atom 与 store 共享同一套选择协议,Subscribe组件内部才能用同一个useSelector处理所有分支。

源码实现:useSelector + 浅比较

SubscribeProps最终由Subscribe组件消费。其完整实现位于 react-table/src/Subscribe.ts:

export function Subscribe< TFeatures extends TableFeatures, TSelected, TSourceValue, >( props: SubscribeProps<TFeatures, TSelected, TSourceValue>, ): ReturnType<FunctionComponent> { const selected = useSelector( // Atom and store share the same selection protocol; union args need a widen for TS. props.source, props.selector as Parameters<typeof useSelector>[1], { compare: shallow, }, ) as TSelected return typeof props.children === 'function' ? (props.children as (state: TSelected) => ReactNode)(selected) : props.children }

从实现可以确认三个关键机制:

  1. 底层复用useSelectorSubscribe只是把@tanstack/react-storeuseSelector封装成 JSX 组件形态,因此它天然遵循 TanStack Store 的订阅与变更检测协议。
  2. 比较策略为shallow:通过compare: shallow做浅比较,只有投影结果变化时才会触发重渲染,这正是"行未变化就不重渲染"的底层保障。
  3. children 双形态:函数形态(render prop)会收到投影值并作为渲染结果;非函数形态则直接渲染静态ReactNode,适合"订阅副作用、不订阅渲染"的场景。

Subscribe对外提供了三组重载(overload),分别对应三个联合分支,见 react-table/src/Subscribe.ts。

两种使用入口:table.Subscribe 与独立 Subscribe 组件

SubscribeProps实际被两个 API 共享:

  • table.Subscribe(实例方法,推荐)useTable返回的 React 面 table 上的订阅方法。源码注释指出:"Fortable.SubscribefromuseTable, prefer that API — it uses overloads so JSX contextual typing works." 即实例方法使用重载,JSX 上下文类型推断更准确,写法上可省略source(默认订阅table.store):
<table.Subscribe selector={(state) => ({ rowSelection: state.rowSelection })}> {({ rowSelection }) => ( <div>Selected rows: {Object.keys(rowSelection).length}</div> )} </table.Subscribe>
  • 独立Subscribe组件:在 cell / header 渲染上下文等拿不到 React 面 table 的地方使用。此时table是 core Table 而非useTable的返回值,需要显式传入sourcetable.storetable.atoms中的某一项),组件本身使用联合 props 类型:
<Subscribe source={table.atoms.rowSelection}> {(rowSelection) => <div>...</div>} </Subscribe>

两条入口的 props 结构完全由SubscribeProps定义,差异仅在于source是否可省略。相关说明见 React Compiler 指南 与 Table State 指南。

实战模式:三类订阅的选型建议

在 basic-subscribe 示例 中,三种模式被组合用于同一张表:

  1. 表头全选:store 模式 + 多切片投影(依赖列过滤、全局过滤、行选择三类状态)。
  2. 行复选框:atom + 投影模式,selector只取当前行选择值。
  3. 全局过滤输入框:订阅table.atoms.globalFilter,把表主体与过滤控件解耦。

选型建议可归纳为:

场景推荐模式原因
需要聚合多个状态切片到一个渲染块SubscribePropsWithStore一次投影,浅比较控制重渲染
只关心某一个完整切片SubscribePropsWithSourceIdentity省略 selector 即恒等投影,代码最简
关心某一切片的子集(如单行、单列)SubscribePropsWithSourceWithSelector重渲染面最小化,性能最优

需要说明的是,SubscribeProps属于"性能优化利器"而非默认选项。Table State 指南 明确建议:通常在useTable默认 selector 造成可见性能问题时再引入table.Subscribe。默认情况下,useTable的 selector 会选择全部已注册状态,组件随任何状态变化重渲染;将 selector 收窄(如() => null)后,配合table.Subscribe把响应式读取下沉到真正需要的地方,即可实现"父组件不因表格状态重渲染、子组件按需重渲染"。

与 React Compiler 的配合

SubscribeProps也是 Table v9 兼容 React Compiler 的关键一环。在嵌套组件只接收稳定的tablerowcell等对象作为 props 时,编译器可能因 props 未变而跳过组件渲染,从而漏掉状态变化。正确的做法是把Subscribe/table.Subscribe放在真正读取状态的组件内部,订阅最小的状态片段(如rowSelection[row.id]),并直接从投影值渲染(详见 React Compiler 指南)。由于useSelector是编译器能够识别的响应式依赖,这种方式可以完全规避React.memo结合方法 getter(如cell.getIsSelected())时"状态依赖被隐藏"的问题。相关性能对比与测试依据可参考该指南中维护的基准说明。

小结

SubscribeProps是 TanStack Table React 精细订阅体系的类型基石,它以三个联合分支完整刻画了"订阅 store / 订阅源 / 订阅源投影"三种形态,配合SubscribeSource对 atom 与 store 的统一抽象,让开发者可以在 React 树中精确控制重渲染边界。理解这一类型别名,是掌握table.Subscribe、独立Subscribe组件乃至 Table v9 原子化状态模型的前提;建议进一步阅读 Table State 指南 了解状态读取全貌,并结合 basic-subscribe 示例 与 kitchen-sink 示例 的完整实现进行验证。

【免费下载链接】table🤖 Headless UI for building powerful tables & datagrids for TS/JS - React-Table, Vue-Table, Solid-Table, Svelte-Table项目地址: https://gitcode.com/gh_mirrors/ta/table

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询