
TanStack Table Preact AppTable 的 selector 订阅机制AppTablePropsWithSelector 接口深度解析【免费下载链接】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/tableAppTablePropsWithSelector是tanstack/preact-table中createTableHook组合式表格 API 的核心类型它定义了table.AppTable组件在「带 selector」模式下的 Props 契约让表格渲染树可以精确订阅状态切片而非整个表格状态。本文围绕该接口的类型参数、两个必备属性、与Subscribe/useSelector的底层联动关系展开并结合仓库源码与 composable-tables 示例 给出可复制的实战用法帮助你写出类型安全、按需重渲染的 Preact 表格。接口定位组合式表格工厂中的根包裹组件AppTablePropsWithSelector定义在 createTableHook.tsx:384-390是createTableHook返回的扩展表格实例上AppTable组件的一支 Props 类型。与之并列的是无 selector 变体AppTablePropsWithoutSelector见 接口文档两者共同组成AppTableComponent的函数重载签名export interface AppTableComponentTFeatures extends TableFeatures { (props: AppTablePropsWithoutSelector): ComponentChildren TSelected( props: AppTablePropsWithSelectorTFeatures, TSelected, ): ComponentChildren }从 createTableHook.tsx:521-526 可以看到AppTable通过函数重载实现「同一组件、两种模式」不传selector时children是普通的ComponentChildren传入selector后children变为接收选中状态的渲染函数。这种设计让根组件既能当作纯 context Provider 使用也能顺带承担局部订阅职责。类型参数与属性契约该接口携带两个泛型参数类型参数约束含义TFeaturesextends TableFeatures当前表格启用的功能集排序、分页、筛选等 feature 的联合类型由createTableHook的features配置绑定TSelected无约束selector 函数返回值的类型即children渲染函数收到的状态切片类型接口只有两个属性且都是必填export interface AppTablePropsWithSelector TFeatures extends TableFeatures, TSelected, { children: (state: TSelected) ComponentChildren selector: (state: TableStateTFeatures) TSelected }selector接收完整的TableStateTFeatures返回任意形状的TSelected。它是订阅的「投影」组件树中只有被投影出来的状态切片变化时才会触发重渲染。children在带 selector 模式下被要求必须是函数区别于无 selector 模式下的普通ComponentChildrenPreact 会在每次选中状态变化时以新的state调用它返回当前要渲染的子树。selector 的底层实现Subscribe 与浅比较AppTablePropsWithSelector描述的并非虚构契约AppTableImpl的实现直接消费这两个属性。createTableHook.tsx:967-999 中AppTable在渲染时先通过TableContext.Provider提供当前表格实例然后分支处理function AppTableImplTAppTableSelected( props: AppTablePropsWithoutSelector | AppTablePropsWithSelectorTFeatures, TAppTableSelected, ): ComponentChildren { const { children, selector: appTableSelector } props as any const currentTable tableRef.current return ( TableContext.Provider value{currentTable} {appTableSelector ? ( currentTable.Subscribe selector{appTableSelector} {(state: TAppTableSelected) (children as (state: TAppTableSelected) ComponentChildren)(state) } /currentTable.Subscribe ) : ( children )} /TableContext.Provider ) }即带 selector 时children 被包进table.Subscribe内以选中状态调用不带 selector 时直接渲染普通 children。这正是AppTablePropsWithSelector.children必须是函数类型的原因。table.Subscribe本身是对 Subscribe.ts 中Subscribe组件的封装其核心只有一行订阅逻辑const selected useSelector(props.source as never, props.selector, { compare: shallow, })这里有两个值得注意的机制订阅源默认是table.storeuseTable在创建表格实例时把Subscribe绑定为「未显式传source时回退到tableInstance.store」见 useTable.ts:140-145因此AppTable的 selector 收到的入参就是完整的TableStateTFeatures。浅比较决定重渲染useSelector使用shallow比较选中值只有选中切片发生引用级变化时才触发订阅方更新。这意味着AppTable的 selector 应尽量返回窄而稳定的切片例如(state) state.pagination避免返回每次都是新引用的宽对象否则浅比较会失效并导致不必要的重渲染。另外注意AppTableImpl是从useMemo(() {...}, [])中创建的稳定组件并通过tableRef.current读取最新表格实例——源码注释createTableHook.tsx:956-964明确说明这是为了避免每次渲染重建组件导致 Preact 卸载重挂整棵子树例如工具栏受控输入框在每次按键后失焦。与 useAppTable 第二参数的呼应AppTablePropsWithSelector的 selector 与useAppTable/useTable的第二个参数selector在概念上同源但职责不同useAppTable(tableOptions, selector)的 selector 决定table.state的形态即整个表格实例暴露给组件树的状态切片table.AppTable selector{...}的 selector 决定AppTable局部渲染分支订阅的状态切片作用于children渲染函数。两者的底层都依赖 useTable.ts:176 的useSelector(rootSource, selector, { compare: shallow })进行浅比较订阅。实践中常用组合是useAppTable不传或传宽 selector 让table.state覆盖常用切片而在AppTable层用窄 selector 精确订阅当前渲染所需的状态。实战示例从文档契约到可运行代码仓库的 composable-tables 示例 完整演示了带 selector 的AppTable用法。它先在 hooks/table.ts 中用createTableHook注册共享 features 与组件export const { createAppColumnHelper, useAppTable, useTableContext, useCellContext, useHeaderContext, } createTableHook({ features: tableFeatures({ columnFilteringFeature, rowPaginationFeature, rowSelectionFeature, rowSortingFeature, sortedRowModel: createSortedRowModel(), filteredRowModel: createFilteredRowModel(), paginatedRowModel: createPaginatedRowModel(), sortFns: { alphanumeric: sortFn_alphanumeric, text: sortFn_text }, filterFns: { includesString: filterFn_includesString, inNumberRange: filterFn_inNumberRange }, }), getRowId: (row) row.id, tableComponents: { PaginationControls, RowCount, TableToolbar }, cellComponents: { SelectCell, TextCell, NumberCell, StatusCell, ProgressCell, RowActionsCell, PriceCell, CategoryCell }, headerComponents: { SortIndicator, ColumnFilter, FooterColumnId, FooterSum }, })随后在组件内创建表格useAppTable第二参数返回完整 state保证table.state可直接访问各切片const table useAppTable( { key: users-table, columns, data, debugTable: true, enableRowSelection: true, }, (state) state, )渲染时使用带 selector 的AppTablechildren解构出当前渲染所需的切片本例订阅了排序以便在表头展示多列排序序号table.AppTable selector{(state) state.sorting} {(sorting) ( div classNametable-container {/* 表头里通过 sorting 判断排序序号 */} {table.getHeaderGroups().map((headerGroup) ( tr key{headerGroup.id} {headerGroup.headers.map((h) ( table.AppHeader header{h} key{h.id} {(header) ( th onClick{header.column.getToggleSortingHandler()} header.FlexRender / header.SortIndicator / header.ColumnFilter / {sorting.length 1 sorting.findIndex((s) s.id header.column.id) -1 ( span classNamesort-order {sorting.findIndex((s) s.id header.column.id) 1} /span )} /th )} /table.AppHeader ))} /tr ))} /div )} /table.AppTable这段代码中selector{(state) state.sorting}正是AppTablePropsWithSelector的类型要求入参为完整TableState返回值为TSelected此处是排序状态切片而 children 的形参sorting被 TypeScript 自动推断为TSelected。createTableHook的 composable-tables 指南 中同样展示了多切片写法selector{(state) ({ pagination, sorting, columnFilters })}把三个切片一次投影进 children。[!IMPORTANT] 使用createAppColumnHelper而非createColumnHelper定义列才能在cell/header/footer渲染函数中拿到预绑定组件如cell.TextCell、header.SortIndicator详见 Composable Tables 指南 与 Table Context 指南。最佳实践与注意事项基于上述源码与示例使用AppTablePropsWithSelector时应遵循以下原则selector 保持窄而稳定尽量返回state.pagination、state.sorting这类单切片或使用返回新对象前确保引用稳定。由于底层是shallow比较宽对象每次重新创建都会触发 children 重渲染得不偿失。children 必须是函数带 selector 模式下 TypeScript 会强制children为(state: TSelected) ComponentChildren这是接口类型约束的体现也是运行时AppTableImpl直接调用children(state)的前提。局部订阅优于全局订阅若只有部分子树需要某个状态切片用AppTable的 selector 局部订阅即可不必让useAppTable的 selector 提前放大table.state两处 selector 可独立配置互不阻塞。留意重渲染范围AppTable的 selector 变化只会重渲染其 children 函数产出的子树AppCell/AppHeader等内部仍可各自用Subscribe精细订阅对应 AppCellPropsWithSelector 与 AppHeaderPropsWithSelector 接口形成「根订阅 叶级订阅」的混合优化策略。context 读取不受 selector 影响AppTable无论是否带 selector 都会通过TableContext.Provider提供表格实例因此tableComponents如PaginationControls内部通过useTableContext()读取表格与table.Subscribe独立订阅的写法不受影响见 table-components.tsx。综上AppTablePropsWithSelector是 TanStack Table Preact 适配器「按需订阅」设计的关键类型入口它以一份清晰的 Props 契约把 selector 投影、浅比较订阅与函数式 children 组合进根组件让表格的渲染树在保持类型安全的同时做到最小范围更新。【免费下载链接】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),仅供参考