ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

TanStack Table 自定义 Feature 开发指南(React):深入 tableFeatures 与 TableFeature 扩展机制

TanStack Table 自定义 Feature 开发指南(React):深入 tableFeatures 与 TableFeature 扩展机制 TanStack Table 自定义 Feature 开发指南React深入 tableFeatures 与 TableFeature 扩展机制【免费下载链接】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/tableTanStack Table v9 将排序、过滤、分页等内置能力拆分为相互独立的 feature并通过features选项配合tableFeatures()声明式启用从而支持 tree-shaking。本文以 React 版tanstack/react-table为例系统讲解如何依据TableFeature类型编写自定义 feature把新的状态、选项与实例 API 无缝融入table、row、column、header、cell等实例并在编写过程中剖析 table-core 的内部结构与运行机制。读完本文你将掌握一套与内置 feature 完全同构的自定义扩展方案可直接复用于分页、密度、行选择之外任何你自己的业务能力。为什么需要自定义 FeatureTanStack Table 追求精简TanStack Table 的核心库内置了排序、过滤、分页等基础 feature。社区经常提出增加更多功能的请求甚至包含精心设计的 PR。但官方始终在持续改进库与保持精简之间保持平衡核心库不应包含大多数场景都用不到的冗余代码。并非每个 PR 都应该被并入核心即使它确实解决了真实问题——当 TanStack Table 解决了你 90% 的需求、只差一点点控制力时这种克制会让你感到些许沮丧。这正是自定义 feature 机制的用武之地。自 v7 起TanStack Table 就一直是高度可扩展的无论使用哪个框架适配器createTable、useTable等返回的table实例都是一个普通的 JavaScript 对象可以随时附加额外属性或 API。传统的做法是使用组合composition方式例如社区中的 Material React Table 就是通过包装useTable的自定义 hook 来扩展 table 实例。在 v9 中TanStack Table 改用features选项通过tableFeatures()声明你的表格使用了哪些 feature。这样只打包你需要的功能代码实现 tree-shaking。自定义 feature 可以用与内置 feature 完全相同的方式挂载到 table 实例上。v9 中 feature 是按需启用的。使用tableFeatures({ ... })声明你的表格启用了哪些 feature包括自定义 feature。TanStack Table Feature 的工作方式TanStack Table 的源码结构相当直白每个 feature 的代码都独立成对象/文件其中包含实例化方法用于创建初始状态initial state、默认表格选项与默认列选项以及会被挂载到table、header、column、row、cell实例上的 API 方法。一个 feature 对象的全部能力都可以用 TanStack Table 导出的TableFeature类型来描述该类型定义于 packages/table-core/src/types/TableFeatures.ts。它本质上是一个描述创建 feature 所需对象形状的 TypeScript 接口export interface TableFeature { assignCellPrototype?: TFeatures extends TableFeatures, TData extends RowData, ( prototype: Recordstring, any, table: Table_InternalTFeatures, TData, ) void assignColumnPrototype?: TFeatures extends TableFeatures, TData extends RowData, ( prototype: Recordstring, any, table: Table_InternalTFeatures, TData, ) void assignHeaderPrototype?: TFeatures extends TableFeatures, TData extends RowData, ( prototype: Recordstring, any, table: Table_InternalTFeatures, TData, ) void assignRowPrototype?: TFeatures extends TableFeatures, TData extends RowData( prototype: Recordstring, any, table: Table_InternalTFeatures, TData, ) void constructTableAPIs?: TFeatures extends TableFeatures, TData extends RowData( table: Table_InternalTFeatures, TData, ) void initTableInstanceData?: TFeatures extends TableFeatures, TData extends RowData, ( table: Table_InternalTFeatures, TData, ) void getDefaultColumnDef?: TFeatures extends TableFeatures, TData extends RowData, TValue extends CellData CellData, () ColumnDefBase_AllTFeatures, TData, TValue getDefaultTableOptions?: TFeatures extends TableFeatures, TData extends RowData, ( table: Table_InternalTFeatures, TData, ) PartialTableOptions_AllTFeatures, TData getInitialState?: (initialState: PartialTableState_All) TableState_All initCellInstanceData?: TFeatures extends TableFeatures, TData extends RowData, TValue extends CellData CellData, ( cell: CellTFeatures, TData, TValue, ) void initColumnInstanceData?: TFeatures extends TableFeatures, TData extends RowData, TValue extends CellData CellData, ( column: ColumnTFeatures, TData, TValue, ) void initHeaderGroupInstanceData?: TFeatures extends TableFeatures, TData extends RowData, ( headerGroup: HeaderGroupTFeatures, TData, ) void initHeaderInstanceData?: TFeatures extends TableFeatures, TData extends RowData, TValue extends CellData CellData, ( header: HeaderTFeatures, TData, TValue, ) void initRowInstanceData?: TFeatures extends TableFeatures, TData extends RowData, ( row: RowTFeatures, TData, ) void resetTableInstanceData?: TFeatures extends TableFeatures, TData extends RowData, ( table: Table_InternalTFeatures, TData, ) void }初次接触这些方法可能会觉得繁琐下面逐个拆解它们的职责。默认选项与初始状态getDefaultTableOptionsgetDefaultTableOptions负责为 feature 设置默认的表格选项。例如 Column Resizing 的getDefaultTableOptions会把columnResizeMode选项的默认值设为onEnd。用户传入的选项优先级高于这里返回的默认值。getDefaultColumnDefgetDefaultColumnDef负责为 feature 设置默认的列选项。例如 Sorting 的getDefaultColumnDef会把sortUndefined列选项的默认值设为1。这些默认值会先于用户的options.defaultColumn与具体列定义被合并因此用户始终可以覆盖。getInitialStategetInitialState负责为 feature 设置默认的 state 切片。例如 Pagination 的getInitialState会把pageSize的默认值设为10、pageIndex的默认值设为0。从源码可以看到它还会保留外部传入的initialState.pagination以支持用户覆盖默认值getInitialState: (initialState) { return { ...initialState, pagination: { ...getDefaultPaginationState(), ...initialState.pagination, }, } }API 构造器initTableInstanceData 与 resetTableInstanceDatainitTableInstanceData用于存放属于单个 table 实例的可变、非响应式数据例如交互锚点或命令式缓存。它在表格选项、state atoms 与 store 创建完成之后运行一次。feature 按注册顺序单趟处理每个 feature 的初始化 hook 都恰好在其constructTableAPIshook 之前运行因此你的 hook 可以依赖更早注册 feature 的数据与 API。resetTableInstanceData用于在table.reset()执行时清理这些临时数据。reset hook 在内部持有的 table state atoms 恢复为table.initialState之后运行它不会重置 table state 切片或外部受控状态并且table.reset()不会重新执行initTableInstanceData。API 的赋值必须放在constructTableAPIs中初始化与 reset hook 只负责管理 feature 自有的数据。constructTableAPIsconstructTableAPIs专门负责向table实例添加方法。它在所有 feature 持有的 table 实例数据初始化完成后运行。例如 Row Selection 的constructTableAPIs添加了toggleAllRowsSelected、getIsAllRowsSelected、getIsSomeRowsSelected等一系列 table 实例 API。所以当你调用table.toggleAllRowsSelected()时实际调用的正是rowSelectionFeature添加到 table 实例上的方法。在 table-core 的实现中这类赋值统一通过assignTableAPIs工具函数完成见 packages/table-core/src/utils.ts。该函数遍历传入的 API 对象把方法直接挂到 table 实例上若提供了memoDeps还会用tableMemo包装出带缓存的版本export function assignTableAPIs(feature, table, apis): void { for (const [staticFnName, { fn, memoDeps }] of Object.entries(apis)) { const { fnKey, fnName } getFunctionNameInfo(staticFnName) ;(table as Recordstring, any)[fnKey] memoDeps ? tableMemo({ memoDeps, fn, fnName, table, feature }) : fn } }与之相对的assignPrototypeAPIs同一文件则把方法挂到共享原型对象上让同一表格的所有行/列/单元格共享同一份方法引用只有 memo 状态按实例懒创建——兼顾共享方法代码与按实例缓存。assignHeaderPrototype 与 initHeaderInstanceDataassignHeaderPrototype负责向共享的header原型添加方法。例如 Column Sizing 添加了getStart等 header 实例 API。所以当你调用header.getStart()时调用的是 column sizing feature 添加的方法。initHeaderInstanceData用于存放无法放在共享原型上的、按 header 实例独立的数据或缓存。它在 header 构造期间、subHeaders填充之前、且 header 尚未关联到其 header group 时运行。header group 一旦重算header 就会被重建因此该 hook 会在每次重建时重新执行。initHeaderGroupInstanceDatainitHeaderGroupInstanceData用于存放按 header group 实例独立的数据。header group没有共享原型因此这是它唯一的按实例扩展点。它在 header group 的depth、id以及填充完整的headers数组都被赋值之后运行并且每当 header group 重建例如列可见性、顺序或固定发生变化时都会重新执行。assignColumnPrototype 与 initColumnInstanceDataassignColumnPrototype负责向共享的column原型添加方法。例如 Sorting 添加了getNextSortingOrder、toggleSorting等 column 实例 API。当你调用column.toggleSorting()时调用的正是 row sorting feature 添加的方法。initColumnInstanceData用于存放按 column 实例独立的数据或缓存。例如 Aggregation 用它为每个列建立独立的聚合缓存。assignRowPrototype 与 initRowInstanceDataassignRowPrototype负责向共享的row原型添加方法。initRowInstanceData用于存放按 row 实例独立的数据或缓存。例如 Row Selection 添加了toggleSelected、getIsSelected等 row 实例 API。assignCellPrototype 与 initCellInstanceDataassignCellPrototype负责向共享的cell原型添加方法。例如 Column Grouping 添加了getIsGrouped与getIsPlaceholderAggregation 添加了getIsAggregated。initCellInstanceData用于存放按 cell 实例独立的数据或缓存。cell 按行/列对在首次访问时懒构造并缓存因此该 hook 对每个 cell 实例只运行一次。动手实现一个自定义 Feature密度Density插件下面我们以一个假设场景走一遍完整流程为 table 实例添加一个允许用户切换表格密度即单元格 padding的 feature。完整实现见 custom-plugin 示例这里按步骤深入拆解。Step 1搭建 TypeScript 类型如果你想获得与内置 feature 同等的完整类型安全首先需要为新 feature 定义类型新的 table options、state 与 table 实例 API 方法。这些类型沿用 TanStack Table 内部的命名约定你也可以自行命名先在本模块定义下一步再接入 TanStack Table 的类型系统。// define types for our new features custom state export type DensityState sm | md | lg export interface TableState_Density { density: DensityState } // define types for our new features table options export interface TableOptions_Density { enableDensity?: boolean onDensityChange?: OnChangeFnDensityState } // Define types for our new features table APIs export interface Table_Density { setDensity: (updater: UpdaterDensityState) void toggleDensity: (value?: DensityState) void }三个接口分别对应 feature 的三个维度TableState_Density描述状态切片TableOptions_Density描述可选配置项Table_Density描述新增到 table 实例上的 API。Step 2把 Feature 注册进 TanStack Table 的 Feature MapTanStack Table 用传给tableFeatures({ ... })的 key 来推断某个 table 上存在哪些 feature 的 state、options 与 APIs。要让自定义 feature 的 key 获得类型安全需要通过**声明合并declaration merging**把它注册到导出的Plugins、TableState_FeatureMap、TableOptions_FeatureMap与Table_FeatureMap接口中declare module tanstack/react-table { interface Plugins { densityPlugin: TableFeature } interface TableState_FeatureMap { densityPlugin: TableState_Density } interface TableOptions_FeatureMap TFeatures extends TableFeatures, TData extends RowData, { densityPlugin: TableOptions_Density } interface Table_FeatureMap TFeatures extends TableFeatures, TData extends RowData, { densityPlugin: Table_Density } }一旦这样注册TypeScript 只会在features包含densityPlugin的 table 上推断出该 feature 的 state、options 与 APIs。Step 3创建 Feature 对象类型搭建完成后就可以创建 feature 对象在这里定义所有要挂到 table 实例上的方法。用TableFeature类型约束对象形状只要类型设置正确用新的 state、options 与实例 API 创建 feature 对象时不会出现任何 TypeScript 错误。export const densityPlugin: TableFeature { // define the new features initial state getInitialState: (initialState) { return { density: md, ...initialState, // must come last } }, // define the new features default options getDefaultTableOptions: (table) { return { enableDensity: true, onDensityChange: makeStateUpdater(density, table), } }, // if you need to add a default column definition... // getDefaultColumnDef: () {}, // define the new features table instance methods constructTableAPIs: (table) { assignTableAPIs(densityPlugin, table, { table_setDensity: { fn: (updater: UpdaterDensityState) { const safeUpdater: UpdaterDensityState (old) { const newState functionalUpdate(updater, old) return newState } return table.options.onDensityChange?.(safeUpdater) }, }, table_toggleDensity: { fn: (value?: DensityState) { const safeUpdater: UpdaterDensityState (old) { if (value) return value return old lg ? md : old md ? sm : lg } return table.options.onDensityChange?.(safeUpdater) }, }, }) }, // if you need to add row instance APIs... // assignRowPrototype: (prototype, table) {}, // initRowInstanceData: (row) {}, // if you need to add cell instance APIs... // assignCellPrototype: (prototype, table) {}, // initCellInstanceData: (cell) {}, // if you need to add column instance APIs... // assignColumnPrototype: (prototype, table) {}, // initColumnInstanceData: (column) {}, // if you need to add header instance APIs... // assignHeaderPrototype: (prototype, table) {}, // initHeaderInstanceData: (header) {}, // if you need to add header group instance data... // initHeaderGroupInstanceData: (headerGroup) {}, }几个关键细节getInitialState中...initialState必须放在最后这样调用方传入的initialState无论来自更早注册的 feature 还是用户都能覆盖density的默认值md。makeStateUpdater(density, table)是 TanStack Table 提供的标准状态更新器工厂见 packages/table-core/src/utils.ts。它返回一个接受Updater的函数若用户在 options 里提供了外部 atom 则写入该 atom否则写入 table 内部的baseAtoms并统一通过functionalUpdate(updater, old)兼容传值与传函数两种更新形式。这就是为什么所有内置 feature 的onXxxChange都能同时支持直接传值或传入更新函数。constructTableAPIs中通过assignTableAPIs注册方法。方法名使用table_setDensity、table_toggleDensity这种静态函数名格式内部会解析出实际挂在 table 上的setDensity、toggleDensity。toggleDensity不带参数时会在sm → md → lg → sm之间循环带参数则直接跳到指定密度。其它原型赋值 hookassignRowPrototype、initCellInstanceData等在示例中保持注释状态需要时按上面API 构造器一节的职责说明逐个启用。Step 4把 Feature 添加到 Table创建好 feature 对象后把它作为参数传给tableFeatures()并将返回值传给useTable的features选项即可const features tableFeatures({ densityPlugin }) const table useTable({ features, columns, data, //.. })tableFeatures()是一个类型辅助函数定义于 packages/table-core/src/helpers/tableFeatures.ts运行时只是原样返回传入对象核心价值在于类型推断与插槽校验除了 feature 模块它还承载排序/过滤/聚合的行模型工厂sortedRowModel、filteredRowModel等、函数注册表sortFns、filterFns、aggregationFns以及类型专用的tableMeta/columnMeta插槽。官方建议在组件外部静态调用它避免每次渲染都重建。在 custom-plugin 示例 中densityPlugin与内置的columnFilteringFeature、rowSortingFeature、rowPaginationFeature一起传入用法完全一致const features tableFeatures({ columnFilteringFeature, rowSortingFeature, rowPaginationFeature, densityPlugin, // pass in our plugin just like any other stock feature filteredRowModel: createFilteredRowModel(), paginatedRowModel: createPaginatedRowModel(), sortedRowModel: createSortedRowModel(), filterFns: { includesString: filterFn_includesString, inNumberRange: filterFn_inNumberRange, }, sortFns: { alphanumeric: sortFn_alphanumeric, text: sortFn_text, }, })Step 5在应用中使用 Featurefeature 挂载后就可以在应用中消费新的实例 API、options 与 state。示例 custom-plugin 用React.useState控制densitystate并通过新的onDensityChange选项把它接回 tableconst features tableFeatures({ densityPlugin }) const [density, setDensity] React.useStateDensityState(md) const table useTable({ features, columns, data, //... state: { density, // passing the density state to the table, TS is still happy :) }, onDensityChange: setDensity, }) //... return ( td key{cell.id} style{{ //using our new feature in the code padding: density sm ? 4px : density md ? 8px : 16px, transition: padding 0.2s, }} table.FlexRender cell{cell} / /td )由于声明合并已经完成把density传入state、把setDensity传给onDensityChange时 TypeScript 都能正确推断同时你依然可以直接调用table.toggleDensity()与table.setDensity(...)这两个由插件新增的实例 API示例中Toggle Density按钮正是这样工作的。至此一个新的、类型安全的自定义 feature 就完整地融入了表格实例。我们一定要用这种方式吗这只是把自定义代码与内置 feature 集成到 TanStack Table 中的一种新方式。在上面的例子里你完全可以把density状态放进React.useState在任意位置定义自己的toggleDensity处理函数再脱离 table 实例单独使用它。在 TanStack Table 旁边构建 feature、而不是把代码深度整合进 table 实例依然是完全可行的自定义方案。具体选择哪种方式取决于你的使用场景——把逻辑并入表格实例并非总是最干净的做法。结论与延伸阅读本文演示的densityPlugin走完了自定义 feature 的完整生命周期类型定义 → 声明合并注册 → feature 对象实现默认 state / 默认 options / 实例 API→tableFeatures()装配 → 应用层消费。这条路径与内置 feature 完全同构因此你可以参考任意内置实现继续深入Column Resizing演示getDefaultTableOptions与assignHeaderPrototype的配合Row Sorting演示getDefaultColumnDef与assignColumnPrototypeRow Pagination演示getInitialState与constructTableAPIs的标准写法Row Selection演示批量 table API 与 row API 的注册custom-plugin 示例本文密度插件的可运行完整版含排序、过滤、分页组合与 100 万行压力测试table-core 工具函数makeStateUpdater、assignTableAPIs、assignPrototypeAPIs、functionalUpdate等支撑机制的具体实现。【免费下载链接】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),仅供参考
返回列表