- 前端
- UI组件
【免费下载链接】table
🤖 Headless UI for building powerful tables & datagrids for TS/JS - React-Table, Vue-Table, Solid-Table, Svelte-Table
导读
SubscribePropsWithSourceWithSelector<TSourceValue, TSelected>是@tanstack/octane-table中Subscribe组件的一种 props 形态,用于让 Octane 组件从某个状态源(TanStack Store 的原子Atom或Store)订阅一个投影后的值:selector接收源值,而children渲染函数接收投影结果TSelected。它支撑了 Octane 表格"只让树中需要刷新的节点精确重渲染"的细粒度订阅模型,是优化大表格性能、实现行选择/分页/过滤等局部响应式 UI 的基础。阅读本文后,你将掌握该接口的完整属性签名、与"恒等订阅""整表订阅"两种形态的区别,以及它在源码层和真实示例中的用法。
该接口定义于 packages/octane-table/src/types.ts,是SubscribeProps联合类型三种形态之一,本文围绕它展开,并结合仓库源码与示例进行纵深讲解。
接口定位:Subscribe 三种 props 形态中的"投影订阅"
在 Octane 适配器中,Subscribe组件(以及useTable返回实例上的table.Subscribe)可以订阅三类状态源,分别对应三种 props 形态(见 types.ts):
| Props 形态 | source 取值 | selector | children 收到 | 典型场景 |
|---|---|---|---|---|
SubscribePropsWithStore<TFeatures, TSelected> | table.store(整表扁平 Store) | 必填,从TableState投影 | TSelected | 只关心分页、过滤等若干状态切片 |
SubscribePropsWithSourceIdentity<TSourceValue> | 单个 Atom 或 Store | 省略(恒等投影) | 完整的TSourceValue | 订阅整个行选择 Atom |
SubscribePropsWithSourceWithSelector<TSourceValue, TSelected> | 单个 Atom 或 Store | 必填 | 投影结果TSelected | 从行选择 Atom 中投影单行选中状态 |
三个接口的完整定义与源码注释见 types.ts,其中本接口的关键注释为:
Subscribe to a projected value from a source (atom or store). The selector receives the source value; children receive the projected
TSelected.
而SubscribeProps本身是这三种形态的联合(types.ts),Subscribe组件的导出类型SubscribeComponent则用重载的可调用类型来表达同一约束(types.ts),以保证 JSX 调用点处selector回调能获得正确的上下文类型推断。
类型参数:TSourceValue与TSelected
该接口声明了TSourceValue与TSelected两个泛型参数:
TSourceValue:状态源所承载的值的类型,即source与selector输入参数的类型。它由source传入的 Atom/Store 推断得出。TSelected:selector的返回类型,也就是children渲染函数实际收到的投影值的类型。它由selector函数的返回类型推断得出。
值得强调的是重载顺序的用意:在 SubscribeComponent 中,source 相关重载排在 store 重载之前,这样TSourceValue能优先从source推断;而"无selector的恒等重载"被单独拆开(SubscribePropsWithSourceIdentity),使得省略selector时children收到的是TSourceValue而非退化为unknown。若把selector做成可选参数放在同一个重载上,TSelected会默认成unknown,丢失推断精度——这是该接口被拆成两个独立形态的根本原因。
三个属性逐一拆解
接口的完整属性签名如下(对应 types.ts):
export interface SubscribePropsWithSourceWithSelector<TSourceValue, TSelected> { source: SubscribeSource<TSourceValue> selector: (state: TSourceValue) => TSelected children: ((state: TSelected) => OctaneNode) | SubscribeStaticChild }source: SubscribeSource<TSourceValue>
订阅的目标状态源,其类型定义于 types.ts:
export type SubscribeSource<TValue> = Atom<TValue> | ReadonlyAtom<TValue> | Store<TValue> | ReadonlyStore<TValue>也就是说,可读可写的Atom、只读的ReadonlyAtom、以及Store/ReadonlyStore均可以作为source。在表格场景中,最常用的取值是:
table.atoms.rowSelection、table.atoms.globalFilter、table.atoms.pagination、table.atoms.columnFilters等按状态切片暴露的只读派生 Atom;table.store(整表扁平 Store,但本接口更鼓励订阅单一 Atom/Store 做局部投影);- 通过
atoms选项传入的外部 Atom(例如useCreateAtom<RowSelectionState>({})创建的实例),此时source可以是在表格作用域之外独立存在的共享原子。
selector: (state: TSourceValue) => TSelected
投影函数。它接收source的完整值,返回投影后的TSelected。Subscribe的实现会把它传给 TanStack Store 的useSelector,因此:
- 只有投影结果发生变化时,订阅者才会重渲染;
- 比较采用浅比较(shallow compare),因此对象字面量形式的 selector 结果(如
{ rowSelection: state.rowSelection })在值未变时不会触发多余的重渲染。
这一点在实现 Subscribe.tsrx 中有直接印证:
const selected = useSelector( props.source as Parameters<typeof useSelector>[0], props.selector as Parameters<typeof useSelector>[1], { compare: shallow }, )shallow比较的引入正是为了与上游保持一致:对象字面量选择器不会在每次状态通知时都触发重渲染。
children: ((state: TSelected) => OctaneNode) | SubscribeStaticChild
children 有两种形态:
- 渲染函数(render prop):
(state: TSelected) => OctaneNode,接收投影后的TSelected并返回 Octane 节点。这是本接口的核心用法——children拿到的不是原始源值,而是selector投影后的值。 - 静态子节点:
SubscribeStaticChild(定义于 types.ts),可以是ElementDescriptor、字符串、数字、布尔值、null/undefined或只读数组。它保持具体类型而非直接使用OctaneNode(当前为unknown),是为了让函数形式的 children 仍能获得上下文类型推断。
实现中(Subscribe.tsrx)对 children 做了区分处理:函数则用投影后的selected调用之,否则原样渲染静态子节点。
底层机制:useSelector+shallow的订阅协议
Subscribe组件被作者用.tsrx文件编写(见 Subscribe.tsrx 的头部注释),因为它内部调用了 slot 键控的 hookuseSelector,编译器会为它在调用点分配渲染 slot。Atom 与 Store 共用同一套选择协议:
- 组件首次渲染时,
useSelector对source执行selector,得到TSelected; - 后续当该 Atom/Store 状态变化时,TanStack Store 重新计算
selector,并用shallow与上一次结果比较; - 若投影结果不同,则仅重渲染该
Subscribe节点及其子树——即"重渲染精确落在需要它的树节点上"。
值得注意的实现细节是:SubscribeImpl的函数体接收的是联合类型SubscribeProps,但导出的绑定被重新标注为重载可调用类型SubscribeComponent(Subscribe.tsrx)。这是因为.tsrx的@{ … }函数体无法携带前置重载签名(TS2384 限制),而 JSX 调用点需要的是重载解析能力,可调用类型恰好提供这一点。调用点仍然获得按重载的逐项推断,只有这一个函数体内部看到联合类型。
table.Subscribe与独立Subscribe的差异
useTable返回的OctaneTable实例上挂载了预绑定好的table.Subscribe(useTable.tsrx):
tableInstance.Subscribe = ((props: any) => ( <Subscribe {...props} source={props.source ?? tableInstance.store} /> )) as OctaneTable<TFeatures, TData, TSelected>['Subscribe']关键行为:
- 不传
source时,table.Subscribe自动以table.store作为源,此时等价于SubscribePropsWithStore形态,必须提供 selector; - 传
source(单个 Atom/Store)时,就落到本文所讲的SubscribePropsWithSourceWithSelector(带 selector)或SubscribePropsWithSourceIdentity(不带 selector)形态。
而独立导出的Subscribe(从@tanstack/octane-table引入,见 index.ts 的export * from './Subscribe.tsrx')没有预绑定的源,必须在source中显式传入table.store或某个 Atom。
useTable本身还通过第二个参数(可选 selector)将投影值暴露在table.state上(useTable.tsrx),并借助createRenderPhaseSource+ 布局副作用实现"渲染阶段暂存、提交后才发布"的 commit 语义——但那是整表级别的订阅,与本接口的"树中局部订阅"互补。关于两种读取策略(直接读快照 vs. 订阅式读取)的取舍,可进一步参考 docs/framework/octane/guide/table-state.md。
实战示例:单行选择投影与行模型局部订阅
仓库中的 examples/octane/basic-subscribe/src/main.tsrx 完整演示了本接口的两种典型用法。
用法一:从行选择 Atom 投影单行状态
function RowCheckbox({ row, table }: { row: Row<typeof features, Person>; table: TableInstance }) @{ // 只订阅该行的选中值,切换一行时仅重渲染该行的复选框 <table.Subscribe source={table.atoms.rowSelection} // 只订阅 row selection atom selector={(rowSelection) => Boolean(rowSelection[row.id])} // 只在该行选择变化时重渲染 > {(selected: boolean) => <IndeterminateCheckbox ariaLabel={`Select row ${row.id}`} checked={selected} disabled={!row.getCanSelect()} indeterminate={row.getIsSomeSelected()} onChange={row.getToggleSelectedHandler()} />} </table.Subscribe> }这正是SubscribePropsWithSourceWithSelector的教科书级场景:source是table.atoms.rowSelection(ReadonlyAtom<RowSelectionState>),selector把整个行选择状态投影为Boolean(rowSelection[row.id])的布尔值,children渲染函数收到的selected就是投影结果。当用户切换一行时,只有该行复选框的订阅节点重渲染,而不是整个表格。
用法二:从 Store 投影分页状态
<table.Subscribe selector={(state) => state.pagination}> {(pagination: PaginationState) => <div className="controls"><Subscribe source={table.store} selector={(state) => state}> {(state: TableState<typeof features>) => <pre><Subscribe source={externalSelection} selector={(selection) => Boolean(selection['1'])} children={(selectedValue) => { const typed: boolean = selectedValue return <span>{String(typed)}</span> }} />这里externalSelection是createAtom<RowSelectionState>({})创建的外部 Atom,TSourceValue推断为RowSelectionState,selector返回boolean,于是children收到的selectedValue被断言为boolean且通过编译。这份测试同时覆盖了"无 selector 的恒等订阅"(children 收到RowSelectionState)与"带 selector 的投影订阅"两种重载,从类型系统层面保证了本接口的推断契约。
使用建议与注意事项
- 先默认、再优化:如 docs/framework/octane/guide/table-state.md 所建议,
useTable默认会选中全部注册状态,日常开发先使用默认行为;只有当大表格出现可感知的性能问题时,再改用table.Subscribe配合本接口做局部订阅。 - 理解状态依赖再订阅:
basic-subscribe示例的注释提醒,很难知道该订阅什么,除非你理解内部 API 的每个状态依赖。务必测试并验证订阅确实在你预期的时机重渲染。 - selector 返回稳定引用:由于订阅用
shallow比较结果,selector 应避免每次返回全新的不等价对象,否则可能引发多余重渲染。 - 外部 Atom 是推荐的控制方式:在 v9 中,需要与应用共享的状态(如服务端数据获取中的分页、排序、过滤)推荐通过
atoms选项传入外部 Atom,再用SubscribePropsWithSourceWithSelector在任意需要的位置精确订阅。
小结
SubscribePropsWithSourceWithSelector<TSourceValue, TSelected>是 Octane 表格细粒度响应式模型的基石之一:source圈定状态源,selector把源值投影为 UI 真正需要的形状,children只接收投影结果。它与SubscribePropsWithSourceIdentity(恒等订阅)、SubscribePropsWithStore(整表订阅)共同构成Subscribe/table.Subscribe的完整 props 契约,其实现建立在 TanStack Store 的useSelector+shallow选择协议之上,类型推断则由重载的可调用类型保证。需要深入源码时,可依次阅读 packages/octane-table/src/types.ts、packages/octane-table/src/Subscribe.tsrx、packages/octane-table/src/useTable.tsrx,并结合 examples/octane/basic-subscribe/src/main.tsrx 与 docs/framework/octane/guide/table-state.md 对照学习。
- 前端
- UI组件
【免费下载链接】table
🤖 Headless UI for building powerful tables & datagrids for TS/JS - React-Table, Vue-Table, Solid-Table, Svelte-Table
相关推荐
深入理解 TanStack Table v9 Octane 适配器的 useTable:签名、状态选择器与源码原理
深入理解 TanStack Table v9 Octane 适配器的 useTable:签名、状态选择器与源码原理 TanStack Table v9 将框架无
前端UI组件CANN/docs PR文档翻译技能
PR文档翻译Skill 功能描述 检测用户本地仓库的md文档,自动翻译成英文文档。支持: 非docs目录下的所有md文档 docs目录下,排除列表外的md文档
前端UI组件TanStack Table Octane 订阅源类型 SubscribeSource 全面解析:Atom 与 Store 联合类型的精确定义与实战
TanStack Table Octane 订阅源类型 SubscribeSource 全面解析:Atom 与 Store 联合类型的精确定义与实战 导读 Su
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考