- 前端
- UI组件
【免费下载链接】table
🤖 Headless UI for building powerful tables & datagrids for TS/JS - React-Table, Vue-Table, Solid-Table, Svelte-Table
SubscribePropsWithSourceWithSelector是@tanstack/react-table中Subscribe组件(订阅高阶组件)面向"单一数据源 + 选择器投影"场景的核心 props 类型。本文以官方类型文档为骨架,结合 Subscribe.ts 源码实现与 basic-subscribe 示例,完整讲解该类型别名的字段契约、类型参数、底层订阅原理,以及如何用它精确控制 React 树中某一部分的重新渲染,实现表格状态的细粒度响应。
类型别名总览:签名与定位
SubscribePropsWithSourceWithSelector定义于 packages/react-table/src/Subscribe.ts:51,其完整签名如下:
type SubscribePropsWithSourceWithSelector<TSourceValue, TSelected> = { source: SubscribeSource<TSourceValue> selector: (state: TSourceValue) => TSelected children: ((state: TSelected) => ReactNode) | ReactNode }官方文档对其定位的描述是:"Subscribe to a projected value from a source (atom or store). The selector receives the source value; children receive the projectedTSelected."——即订阅某个数据源(原子 atom 或存储 store)的投影值:selector接收源值并计算出投影结果,children拿到的是投影后的TSelected,而不是完整的源值。
该类型与另外三个变体共同构成Subscribe的完整 props 契约族,它们都定义在同一个 Subscribe.ts 文件中:
| 类型别名 | 数据源 | selector | children 收到的值 | 源码位置 |
|---|---|---|---|---|
SubscribePropsWithStore | table.store(完整TableState) | 必填 | TSelected(投影自完整表格状态) | Subscribe.ts:20 |
SubscribePropsWithSourceIdentity | 单个 atom/store | 可省略(等价恒等投影) | TSourceValue(源值本身) | Subscribe.ts:41 |
SubscribePropsWithSourceWithSelector | 单个 atom/store | 必填 | TSelected(投影值) | Subscribe.ts:51 |
SubscribePropsWithSource | 单个 atom/store | 可选(上述两者的联合) | TSelected = TSourceValue | Subscribe.ts:62 |
其中SubscribePropsWithSource正是"恒等模式"与"投影模式"的联合类型:SubscribePropsWithSourceIdentity<TSourceValue> | SubscribePropsWithSourceWithSelector<TSourceValue, TSelected>(见 SubscribePropsWithSource 文档)。而最终汇总的SubscribeProps则在此基础上再并入 store 模式(Subscribe.ts:66)。
三个属性逐一拆解
source:订阅的数据源
source: SubscribeSource<TSourceValue>;source是订阅对象,其类型SubscribeSource<TValue>定义于 Subscribe.ts:13:
export type SubscribeSource<TValue> = | Atom<TValue> | ReadonlyAtom<TValue> | Store<TValue> | ReadonlyStore<TValue>也就是说,source可以是atom(原子状态)或store(存储),可读即可订阅。在表格场景中,常见的取值包括:
table.atoms.rowSelection—— 行选择状态原子;table.atoms.globalFilter/table.atoms.columnFilters—— 全局过滤与列过滤原子;table.optionsStore—— 表格配置项的 store。
在SubscribePropsWithSourceWithSelector中,TSourceValue即该数据源承载的值的类型,selector将基于它进行投影计算。
selector:投影函数(必填)
selector: (state: TSourceValue) => TSelected;selector接收完整源值state,返回投影结果TSelected。这是本类型与SubscribePropsWithSourceIdentity的本质区别:
SubscribePropsWithSourceIdentity中selector被类型化为undefined(可省略),省略即等价于恒等投影,children 直接收到完整的TSourceValue;SubscribePropsWithSourceWithSelector中selector必填,从而让开发者明确声明"只关心源值的哪一部分"。
这种设计(store 模式与投影模式均强制要求 selector)在源码注释中有明确动机:"Required in store mode so you never accidentally subscribe to the whole store without an explicit projection."——防止开发者在不经意间订阅了整个 store 而没有显式投影,导致不必要的重渲染(见 Subscribe.ts:29 附近注释)。
children:渲染函数或普通节点
children: ((state: TSelected) => ReactNode) | ReactNode;children有两种合法形态:
- 函数形态
(state: TSelected) => ReactNode:接收投影值TSelected,返回需要渲染的内容。这是性能优化的关键形态——只有投影值变化时,该函数体才会重新执行; - 普通节点形态
ReactNode:不消费投影值,仅作为静态内容,适合只需触发订阅、不关心具体值的场景(例如只想知道"某状态变了")。
函数形态在运行时由 Subscribe.ts:147-149 判断并调用:
return typeof props.children === 'function' ? (props.children as (state: TSelected) => ReactNode)(selected) : props.children类型参数:TSourceValue 与 TSelected
本类型别名携带两个泛型参数:
| 类型参数 | 含义 | 推导来源 |
|---|---|---|
TSourceValue | 数据源承载的原始值类型 | 由source: SubscribeSource<TSourceValue>自动推导 |
TSelected | 投影结果类型 | 由selector: (state: TSourceValue) => TSelected的返回值推导 |
得益于 TypeScript 的上下文类型推导,绝大多数场景无需显式书写泛型参数。在 useTable.ts 中,table.Subscribe专门为投影模式设置了独立重载:
Subscribe: { <TSourceValue>(props: { source: SubscribeSource<TSourceValue> selector?: undefined children: ((state: TSourceValue) => ReactNode) | ReactNode }): ReturnType<FunctionComponent> <TSourceValue, TSubSelected>(props: { source: SubscribeSource<TSourceValue> selector: (state: TSourceValue) => TSubSelected children: ((state: TSubSelected) => ReactNode) | ReactNode }): ReturnType<FunctionComponent> <TSubSelected>( props: Omit<SubscribePropsWithStore<TFeatures, TSubSelected>, 'source'>, ...源码注释解释了这一设计:使用重载(overloads)而非单个联合类型,是为了让 JSX 中的selector回调获得正确的上下文类型;若将两个 selector 签名合并为联合,类型会退化推导为隐式any。同时,"无 selector"与"有 selector"拆分为两个重载,使省略selector时 children 能正确收到TSourceValue(恒等投影),否则TSubSelected会默认推导为unknown而非从 source 推断。
对比而言,独立导出的Subscribe组件(Subscribe.ts:122)同样通过三个重载覆盖三种模式,但其 props 采用联合类型;而table.Subscribe实例方法则优先推荐使用——因为其重载设计能让 JSX 上下文类型推导更准确(见 Subscribe.ts:80-81 注释)。
底层实现原理:useSelector + shallow 比较
理解了类型契约后,来看Subscribe组件究竟如何把source+selector变成受控的重渲染。核心实现在 Subscribe.ts:137-150:
export function Subscribe<TSourceValue>( props: SubscribeProps<...>, ): ReturnType<FunctionComponent> { const selected = useSelector( 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 }关键点有三:
- 直接复用
@tanstack/react-store的useSelector(导入见 Subscribe.ts:3):atom 与 store 共享同一套选择协议,所以Subscribe无需区分数据源类型,统一走useSelector订阅; - 使用
shallow浅比较:只有投影结果在浅比较下发生变化时,组件才会触发重渲染,这是细粒度订阅的性能根基。也正因如此,selector返回值若为对象字面量,每次执行都会产生新引用,浅比较会判定"变化"——在选择投影内容时需要留意这一点; source需要做一次类型拓宽(as Parameters<typeof useSelector>[1]):源码注释说明联合类型参数在 TS 中需要拓宽才能通过编译(见 Subscribe.ts:139-141)。
整个文件顶部的'use client'指令(Subscribe.ts:1)表明该组件是面向客户端渲染的 React 组件,适用于 RSC(React Server Components)架构下的客户端交互部分。
实战示例:basic-subscribe 中的细粒度订阅
仓库的 basic-subscribe 示例 是官方对该 API 的最佳实战演示。示例注释明确指出:"Subscribe/table.Subscribe 是一个高阶组件,允许订阅表格状态或单个 atom/store,确保重渲染只发生在 React 树中真正需要的位置;建议仅在遇到特定性能问题时使用这些模式。"
场景一:单行选择状态的投影订阅
在行选择列中,每一行的复选框只关心"本行是否被选中",因此订阅rowSelection原子并投影出当前行的值(main.tsx:84-101):
<Subscribe source={table.atoms.rowSelection} // 只订阅行选择原子 selector={(rowSelection) => rowSelection[row.id]} // 只投影当前行 > {(isRowSelected) => ( <IndeterminateCheckbox checked={!!isRowSelected} disabled={!row.getCanSelect()} indeterminate={row.getIsSomeSelected()} onChange={row.getToggleSelectedHandler()} /> )} </Subscribe>这正是SubscribePropsWithSourceWithSelector的教科书级用法:源值是整张RowSelectionState表,投影值却精确到单行,勾选某一行时只有该行的复选框重渲染。
场景二:表头按需订阅 store 子集
表头的全选复选框需要知道列过滤、全局过滤与行选择状态,于是以table.store为源、投影出这三个字段(main.tsx:64-81):
<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>场景三:table.Subscribe 实例方法形态
同一个table实例上,订阅全局过滤原子驱动搜索框(main.tsx:177-186):
<table.Subscribe source={table.atoms.globalFilter}> {(globalFilter) => ( <DebouncedInput value={globalFilter ?? ''} onChange={(value) => table.setGlobalFilter(value)} placeholder="Search all columns..." /> )} </table.Subscribe>省略selector即走SubscribePropsWithSourceIdentity路径,children 直接收到globalFilter原值。示例还对表格主体做了"仅订阅过滤与分页"的投影(main.tsx:213-219),行选择状态则完全由各行的独立订阅接管,从而实现百万行压测下的最小化重渲染。
使用建议与注意事项
综合文档、源码与示例,使用SubscribePropsWithSourceWithSelector时应把握以下原则:
- 先测量再优化:官方示例明确建议仅在遇到具体性能问题时才使用订阅模式(main.tsx:53),默认
useTable的响应式渲染已经足够; - 优先
table.Subscribe:实例方法基于重载实现,JSX 上下文类型推导更精准(useTable.ts:67-76);独立Subscribe组件适用于表格实例不在当前作用域(如 header/cell 组件内部)的场景; - selector 必填是有意为之:投影模式强制显式声明关注的数据切片,避免意外订阅整个 store 造成全量重渲染;
- 留意浅比较语义:
shallow比较意味着每次 selector 返回新对象引用都会被判定为"变化",因此投影目标应尽量是原始类型或稳定引用; - children 函数形态才有性能收益:只有函数 children 才会接收投影值并在值变化时重新执行;静态
ReactNode只承担"订阅触发"职责。
相关资源索引
- 本文主题类型别名:
SubscribePropsWithSourceWithSelector(源码位置) - 姊妹类型:SubscribePropsWithSourceIdentity、SubscribePropsWithSource、SubscribePropsWithStore、SubscribeSource
- 组件实现与导出:packages/react-table/src/Subscribe.ts、packages/react-table/src/index.ts
table.Subscribe重载定义:packages/react-table/src/useTable.ts- 完整可运行示例:examples/react/basic-subscribe/src/main.tsx
- 前端
- UI组件
【免费下载链接】table
🤖 Headless UI for building powerful tables & datagrids for TS/JS - React-Table, Vue-Table, Solid-Table, Svelte-Table
相关推荐
TanStack Table React 中 SubscribePropsWithSource 类型解析:细粒度订阅 Atom 与 Store 的强类型方案
TanStack Table React 中 SubscribePropsWithSource 类型解析:细粒度订阅 Atom 与 Store 的强类型方案 本
前端UI组件深入解析 TanStack Table React 的 SubscribePropsWithStore:基于 table.store 的细粒度状态订阅
深入解析 TanStack Table React 的 SubscribePropsWithStore:基于 table.store 的细粒度状态订阅 导读 S
前端UI组件Preact Table 细粒度状态订阅:SubscribeProps 类型别名与 Subscribe 组件全面解析
Preact Table 细粒度状态订阅:SubscribeProps 类型别名与 Subscribe 组件全面解析 导读 在基于 @tanstack/prea
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考