- 前端
- UI组件
【免费下载链接】table
🤖 Headless UI for building powerful tables & datagrids for TS/JS - React-Table, Vue-Table, Solid-Table, Svelte-Table
AppPreactTable是 TanStack Table Preact 适配层中由useAppTable返回的扩展表格类型,它在标准 PreactTable 实例之上挂载了AppTable、AppCell、AppHeader、AppFooter四个包装组件以及注册的 tableComponents,用于在 Preact 应用中构建共享特性、共享组件注册表的组合式表格。阅读本文后,你将掌握这一类型的完整结构、六个泛型参数的含义,以及如何基于createTableHook+useAppTable写出类型安全、可复用的应用级表格。
类型定义:从 PreactTable 到 AppPreactTable
AppPreactTable的类型别名定义如下(来源:createTableHook.tsx):
type AppPreactTable<TFeatures, TData, TSelected, TTableComponents, TCellComponents, THeaderComponents> = PreactTable<TFeatures, TData, TSelected> & NoInfer<TTableComponents> & object;它的构成可以拆解为三部分:
PreactTable<TFeatures, TData, TSelected>:useTable返回的基础表格类型,它Omit掉 core 中Table的原始store字段,并补充了 Preact 特有的SubscribeHOC、FlexRender组件以及state只读状态(useTable.ts)。table.state的类型由第二个参数传入的 selector 决定,可能与完整表格状态结构不一致。NoInfer<TTableComponents>:通过createTableHook注册的表格级组件被交叉进表格实例,且在类型层面使用NoInfer避免泛型推断干扰。这意味着table.PaginationControls、table.RowCount这类自定义组件会直接出现在表格对象上,类型完全可感知。object:携带四个 App 包装组件成员——AppTable、AppCell、AppHeader、AppFooter。
文档对该类型的定位是一句话:"Extended table API returned by useAppTable with all App wrapper components",即useAppTable返回的、附带全部 App 包装组件的扩展表格 API。
App 组件体系:四个包装组件各自解决什么问题
四个 App 包装组件本质上都是"Provider + 可选 Subscribe"的组合:它们把表格、单元格、表头实例写入 Preact Context,同时把注册的组件绑定到实例对象上,让子组件通过上下文读取实例并直接渲染注册组件。下面逐一展开。
AppTable:根级上下文 Provider
AppTable: AppTableComponent<TFeatures>;AppTable是根包装组件,负责提供表格上下文,并可选地接受 selector 实现订阅(Subscribe 功能)。它的两种用法(无 selector / 有 selector)如下:
// Without selector - children is ComponentChildren <table.AppTable> <table>...</table> </table.AppTable> // With selector - children receives selected state <table.AppTable selector={(s) => s.pagination}> {(pagination) => <div>Page {pagination.pageIndex}</div>} </table.AppTable>从源码看(createTableHook.tsx),AppTableImpl的核心逻辑是:
- 用
TableContext.Provider包裹 children,value 为tableRef.current(当前表格实例); - 若传入了 selector,则用
<currentTable.Subscribe selector={...}>订阅表格 store,并把选中状态作为参数传给函数式 children; - 若未传 selector,children 直接作为普通 JSX 子节点渲染。
这里的关键实现细节是tableRef 模式:useTable每次渲染都会返回新的 table 引用(这是 React Compiler 兼容性的要求),因此 App 包装组件不能依赖该引用,否则每次状态更新都会导致整个子树被重建(典型症状是工具栏受控输入框每次按键都失焦)。源码通过useRef保存当前 table 并每次渲染刷新tableRef.current,而AppTable本身用useMemo(..., [])创建一次、保持稳定。
AppCell:单元格上下文 + 预绑定 cellComponents
AppCell: AppCellComponent<TFeatures, TData, NoInfer<TCellComponents>>;AppCell包装一个单元格,提供带预绑定cellComponents的单元格上下文,同样可选 selector。文档给出两种用法:
// Without selector <table.AppCell cell={cell}> {(c) => <td><c.TextCell /></td>} </table.AppCell> // With selector - children receives cell and selected state <table.AppCell cell={cell} selector={(s) => s.columnFilters}> {(c, filters) => <td>{filters.length}</td>} </table.AppCell>实现上(createTableHook.tsx),AppCellImpl做了三件事:
- 通过
Object.assign(cell, { FlexRender: CellFlexRender, ...cellComponents })把注册的 cellComponents 和上下文绑定的FlexRender直接挂到 cell 实例上——这就是 children 里c.TextCell、c.FlexRender的来源; - 用
CellContext.Provider提供原始 cell(未扩展的实例),使useCellContext()可读取; - 有 selector 时用
currentTable.Subscribe订阅并将选中状态作为 children 的第二个参数传入。
这个扩展后的上下文类型对应文档中的 AppCellContext:cell字段类型为Cell<TFeatures, TData, TValue> & TCellComponents & { FlexRender: () => ComponentChildren },同时保留column、getValue、renderValue、row、table等标准字段。
AppHeader:表头上下文 + 预绑定 headerComponents
AppHeader: AppHeaderComponent<TFeatures, TData, NoInfer<THeaderComponents>>;AppHeader包装一个表头(Header),提供带预绑定headerComponents的表头上下文:
// Without selector <table.AppHeader header={header}> {(h) => <th><h.SortIndicator /></th>} </table.AppHeader> // With selector <table.AppHeader header={header} selector={(s) => s.sorting}> {(h, sorting) => <th>{sorting.length} sorted</th>} </table.AppHeader>实现逻辑与AppCell对称(createTableHook.tsx):Object.assign(header, { FlexRender: HeaderFlexRender, ...headerComponents })后通过HeaderContext.Provider提供,有 selector 时走 Subscribe。对应的扩展上下文类型见 AppHeaderContext:header字段为Header<TFeatures, TData, TValue> & THeaderComponents & { FlexRender },外加column与table。
AppFooter:表尾的"复用"包装
AppFooter: AppHeaderComponent<TFeatures, TData, NoInfer<THeaderComponents>>;值得注意:AppFooter的类型与AppHeader完全一致——因为表尾在 TanStack Table 中同样使用Header实例表达。它包装一个表尾,提供带预绑定headerComponents的表头上下文(表尾组件复用 headerComponents 注册表):
<table.AppFooter header={footer}> {(f) => <td><table.FlexRender footer={footer} /></td>} </table.AppFooter>从源码看(createTableHook.tsx),AppFooterImpl与AppHeaderImpl结构相同,唯一的差异是把FlexRender绑定为FooterFlexRender(内部读取 footer 并调用<FlexRender footer={header} />)。这也是文档注释"Wraps a footer and provides header context with pre-bound headerComponents"的由来。
六个类型参数详解
| 泛型参数 | 约束 | 含义 |
|---|---|---|
TFeatures | extends TableFeatures | 该表启用的特性集合(如 rowSortingFeature、rowPaginationFeature),由createTableHook的features选项固化 |
TData | extends RowData | 行数据类型,通常由useAppTable的data选项自动推断 |
TSelected | 无约束 | 通过useAppTable第二个参数(selector)选中的状态切片类型,默认TableState<TFeatures> |
TTableComponents | extends Record<string, ComponentType<any>> | 注册的表格级组件映射,交叉进表格实例本身 |
TCellComponents | extends Record<string, ComponentType<any>> | 注册的单元格级组件映射,绑定到 cell 实例 |
THeaderComponents | extends Record<string, ComponentType<any>> | 注册的表头/表尾级组件映射,绑定到 header 实例 |
组件映射统一以Record<string, ComponentType<any>>约束,意味着你在createTableHook中传入的任何 Preact 组件对象都会获得完整的类型感知。TData由useAppTable从数据推断,这正是"每个表只关心自己的列和数据"这一组合式设计的关键。
源码视角:useAppTable 如何组装扩展表格
useAppTable是createTableHook返回的核心 hook(createTableHook.tsx),其组装过程可分为四步:
- 合并默认选项:
{ ...defaultTableOptions, ...tableOptions }——createTableHook传入的选项(含features)成为默认值,useAppTable调用处传入的选项优先覆盖; - 调用 useTable:内部委托
useTable<TFeatures, TData, TSelected>构建基础表格,selector 原样传递,控制table.state的类型与订阅范围; - 稳定化包装组件:
AppTable、AppCell、AppHeader、AppFooter各自通过useMemo(..., [])只创建一次,内部一律通过tableRef.current读取最新表格,避免状态更新引发的子树重挂载; - 合并扩展成员:
Object.assign(table, { AppTable, AppCell, AppHeader, AppFooter, ...tableComponents })得到最终的AppPreactTable实例。
同时,createTableHook还会返回createAppColumnHelper(预绑定TFeatures与注册组件的列辅助器)、useTableContext/useCellContext/useHeaderContext(从对应 Provider 读取实例的钩子)。注册组件内部正是通过这三个上下文钩子访问实例的,例如:
function PaginationControls() { const table = useTableContext() return ( <table.Subscribe selector={(s) => s.pagination}> {(pagination) => ( <div> <button onClick={() => table.previousPage()}>Prev</button> <span>Page {pagination.pageIndex + 1}</span> <button onClick={() => table.nextPage()}>Next</button> </div> )} </table.Subscribe> ) } function TextCell() { const cell = useCellContext<string>() return <span>{cell.getValue()}</span> } function SortIndicator() { const header = useHeaderContext() const sorted = header.column.getIsSorted() return sorted === 'asc' ? '🔼' : sorted === 'desc' ? '🔽' : null }完整实战:组合式表格的三种典型写法
写法一:仅共享特性与默认选项(不使用 App 组件)
最小化的createTableHook只做特性共享,返回的表格仍可用标准useTable的渲染方式输出。先在模块级创建工厂(composable-tables 指南):
import { createSortedRowModel, createTableHook, rowSortingFeature, sortFns, tableFeatures, } from '@tanstack/preact-table' const features = tableFeatures({ rowSortingFeature, sortedRowModel: createSortedRowModel(), sortFns, }) const { useAppTable, createAppColumnHelper } = createTableHook({ features, debugTable: true, enableSortingRemoval: false, }) const columnHelper = createAppColumnHelper<Person>() const columns = columnHelper.columns([ columnHelper.accessor('firstName', { cell: (info) => info.getValue() }), columnHelper.accessor('age', { header: 'Age' }), ])随后useAppTable返回的实例拥有AppTable等成员,但你也可以完全忽略它们,用普通表格 API 渲染:
function UsersTable({ data }: { data: Person[] }) { const table = useAppTable( { key: 'users-table', columns, data }, (state) => ({ sorting: state.sorting }), ) return ( <table> <thead> {table.getHeaderGroups().map((headerGroup) => ( <tr key={headerGroup.id}> {headerGroup.headers.map((header) => ( <th key={header.id} onClick={header.column.getToggleSortingHandler()}> {header.isPlaceholder ? null : <table.FlexRender header={header} />} </th> ))} </tr> ))} </thead> <tbody> {table.getRowModel().rows.map((row) => ( <tr key={row.id}> {row.getAllCells().map((cell) => ( <td key={cell.id}><table.FlexRender cell={cell} /></td> ))} </tr> ))} </tbody> </table> ) }写法二:注册组件 + App 组件渲染(推荐)
当多个表格需要共享工具栏、单元格渲染器、表头渲染器时,把组件注册进createTableHook。完整示例见 examples/preact/composable-tables,其共享配置集中在src/hooks/table.ts:
const { useAppTable, createAppColumnHelper, useTableContext, useCellContext, useHeaderContext } = createTableHook({ features, getRowId: (row) => row.id, tableComponents: { PaginationControls, RowCount, TableToolbar }, cellComponents: { TextCell, NumberCell, StatusCell, ProgressCell, RowActionsCell, PriceCell, CategoryCell }, headerComponents: { SortIndicator, ColumnFilter, FooterColumnId, FooterSum }, })列定义中直接引用注册组件,cell/header/footer渲染函数的参数即AppCellContext/AppHeaderContext,因此cell.TextCell、props.column.id都有完整类型:
const personColumnHelper = createAppColumnHelper<Person>() const columns = useMemo( () => personColumnHelper.columns([ personColumnHelper.accessor('firstName', { header: 'First Name', footer: (props) => props.column.id, cell: ({ cell }) => <cell.TextCell />, }), personColumnHelper.accessor('age', { header: 'Age', footer: (props) => props.column.id, cell: ({ cell }) => <cell.NumberCell />, }), personColumnHelper.display({ id: 'actions', header: 'Actions', cell: ({ cell }) => <cell.RowActionsCell />, }), ]), [], )渲染时,useAppTable返回的表格对象上直接出现table.AppTable、table.AppHeader、table.AppCell、table.TableToolbar、table.PaginationControls——这正是AppPreactTable类型所描述的结构:
const table = useAppTable( { key: 'users-table', columns, data, debugTable: true }, (state) => state, ) return ( <table.AppTable selector={(state) => ({ pagination: state.pagination, sorting: state.sorting, columnFilters: state.columnFilters, })} > {({ sorting, columnFilters }) => ( <div className="table-container"> <table.TableToolbar title="Users Table" onRefresh={refreshData} /> <table> <thead> {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 /> </th> )} </table.AppHeader> ))} </tr> ))} </thead> <tbody> {table.getRowModel().rows.map((row) => ( <tr key={row.id}> {row.getAllCells().map((c) => ( <table.AppCell cell={c} key={c.id}> {(cell) => ( <td> <cell.FlexRender /> </td> )} </table.AppCell> ))} </tr> ))} </tbody> </table> <table.PaginationControls /> <table.RowCount /> </div> )} </table.AppTable> )注意table.AppTable的 selector 一次订阅了pagination、sorting、columnFilters三个切片,函数式 children 直接解构使用;而AppHeader/AppCell内部的注册组件通过上下文钩子各自消费实例,无需手动传递。
写法三:嵌套隔离场景下的 scoped context
默认情况下createTableHook使用模块级共享 Context(HMR 稳定),兄弟表格之间天然隔离。但若需要嵌套两个不同配置的表格(例如外层表格的表头组件里渲染内层表格),共享 Context 会让内层消费者静默解析到最近的外层 Provider。此时使用createTableHookContexts创建独立上下文并传入:
const features = tableFeatures({ rowSelectionFeature }) const { tableContext, cellContext, headerContext } = createTableHookContexts<typeof features>() export const app = createTableHook({ features, tableContext, cellContext, headerContext, })最佳实践与常见陷阱
模块级创建工厂,不要在渲染函数内创建。createTableHook应放在模块作用域:在组件内调用会让 hook 配置与组件注册表每次渲染都不稳定,导致整个表格子树反复重建。
不要为单张表抽象。工厂的价值在于集中重复的策略(特性、行模型、默认选项、组件约定)。只有一张表时应直接用useTable({ features, columns, data })。
上下文钩子必须在对应包装组件内使用。useTableContext必须在<table.AppTable>内、useCellContext必须在<table.AppCell>内、useHeaderContext必须在<table.AppHeader>/<table.AppFooter>内,否则会抛出运行时错误(源码中均有显式检查与错误信息)。
把钩子从调用createTableHook的同一模块导入。只有那里返回的useTableContext/useCellContext/useHeaderContext才携带你的TFeatures与注册组件映射,从而让table.PaginationControls、cell.TextCell、header.SortIndicator获得完整类型推断。
熟悉配套类型。与AppPreactTable协同工作的类型还包括 AppColumnHelper、AppColumnDefBase、CreateTableHookOptions 以及createTableHook函数文档(docs/framework/preact/reference/functions/createTableHook.md),组合阅读可以完整覆盖从"创建工厂"到"定义列"再到"渲染表格"的全链路类型契约。
- 前端
- UI组件
【免费下载链接】table
🤖 Headless UI for building powerful tables & datagrids for TS/JS - React-Table, Vue-Table, Solid-Table, Svelte-Table
相关推荐
TextPathView性能优化:5个技巧解决多View同时动画卡顿问题
TextPathView性能优化:5个技巧解决多View同时动画卡顿问题 TextPathView是一款强大的Android文字路径动画库,能够为应用添加炫酷的
前端UI组件@tanstack/alpine-table 中 AppColumnHelper 类型详解:Alpine 组合式表格的列定义类型契约
@tanstack/alpine table 中 AppColumnHelper 类型详解:Alpine 组合式表格的列定义类型契约 本文以 @tanstack
前端UI组件TDesign Vue Next 表格组件类型扩展问题解析
TDesign Vue Next 表格组件类型扩展问题解析 在使用 TDesign Vue Next 的 Table 组件时,开发者经常会遇到类型定义上的困惑。
前端UI组件桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考