自定义功能指南
示例
想跳过实现吗?看看这些示例:
自定义功能指南
在本指南中,我们将介绍如何使用自定义功能扩展 TanStack Table,在此过程中,我们将更多地了解 TanStack Table v8 代码库的结构和工作原理。
TanStack Table 力求精简
TanStack Table 拥有一套内置的核心功能,例如排序、筛选、分页等。我们收到了许多请求,有时甚至是一些经过深思熟虑的 PR,要求向库中添加更多功能。虽然我们始终乐于改进库,但我们也希望确保 TanStack Table 仍然是一个精简的库,不包含太多冗余和在大多数用例中不太可能使用的代码。并非每个 PR 都能或应该被核心库接受,即使它确实解决了实际问题。对于 TanStack Table 解决了 90% 的用例但他们需要更多控制的开发者来说,这可能会令人沮丧。
TanStack Table 一直以来都以高度可扩展的方式构建(至少从 v7 开始)。你正在使用的任何框架适配器(useReactTable、useVueTable 等)返回的 table 实例都是一个普通的 JavaScript 对象,可以向其添加额外的属性或 API。始终可以使用组合来向表格实例添加自定义逻辑、状态和 API。像 Material React Table 这样的库只是简单地在 useReactTable 钩子周围创建了自定义包装钩子,以使用自定义功能扩展表格实例。
然而,从版本 8.14.0 开始,TanStack Table 公开了一个新的 _features 表格选项,允许你以与内置表格功能已集成的方式完全相同的方式,更紧密、更清晰地将自定义代码集成到表格实例中。
TanStack Table v8.14.0 引入了一个新的 _features 选项,允许你向表格实例添加自定义功能。
通过这种新的更紧密的集成,你可以轻松地向表格添加更复杂的自定义功能,甚至可能将其打包并与社区共享。我们将看看这会随着时间的推移如何演变。在未来的 v9 版本中,我们甚至可以通过使所有功能都可选来降低 TanStack Table 的捆绑大小,但这仍在探索中。
TanStack Table 功能如何工作
TanStack Table 的源代码可以说有点简单(至少我们是这么认为的)。每个功能的所有代码都拆分到自己的对象/文件中,其中包含用于创建初始状态、默认表格和列选项以及可以添加到 table、header、column、row 和 cell 实例的 API 方法的实例化方法。
功能对象的所有功能都可以用从 TanStack Table 导出的 TableFeature 类型来描述。此类型是一个 TypeScript 接口,描述了创建功能所需的功能对象的形状。
export interface TableFeature<TData extends RowData = any> {
createCell?: (
cell: Cell<TData, unknown>,
column: Column<TData>,
row: Row<TData>,
table: Table<TData>,
) => void;
createColumn?: (column: Column<TData, unknown>, table: Table<TData>) => void;
createHeader?: (header: Header<TData, unknown>, table: Table<TData>) => void;
createRow?: (row: Row<TData>, table: Table<TData>) => void;
createTable?: (table: Table<TData>) => void;
getDefaultColumnDef?: () => Partial<ColumnDef<TData, unknown>>;
getDefaultOptions?: (table: Table<TData>) => Partial<TableOptionsResolved<TData>>;
getInitialState?: (initialState?: InitialTableState) => Partial<TableState>;
}
这可能有点令人困惑,所以让我们分解一下这些方法的作用:
默认选项和初始状态
getDefaultOptions
表格功能中的 getDefaultOptions 方法负责为该功能设置默认表格选项。例如,在列大小调整功能中,getDefaultOptions 方法将默认的 columnResizeMode 选项设置为默认值 "onEnd"。
getDefaultColumnDef
表格功能中的 getDefaultColumnDef 方法负责为该功能设置默认列选项。例如,在排序功能中,getDefaultColumnDef 方法将默认的 sortUndefined 列选项设置为默认值 1。
getInitialState
表格功能中的 getInitialState 方法负责为该功能设置默认状态。例如,在分页功能中,getInitialState 方法将默认的 pageSize 状态设置为值 10,并将默认的 pageIndex 状态设置为值 0。
API 创建者
createTable
表格功能中的 createTable 方法负责向 table 实例添加方法。例如,在行选择功能中,createTable 方法添加了许多表格实例 API 方法,例如 toggleAllRowsSelected、getIsAllRowsSelected、getIsSomeRowsSelected 等。因此,当你调用 table.toggleAllRowsSelected() 时,你正在调用由 RowSelection 功能添加到表格实例的方法。
createHeader
表格功能中的 createHeader 方法负责向 header 实例添加方法。例如,在列大小调整功能中,createHeader 方法添加了许多标题实例 API 方法,例如 getStart 以及许多其他方法。因此,当你调用 header.getStart() 时,你正在调用由 ColumnSizing 功能添加到标题实例的方法。
createColumn
表格功能中的 createColumn 方法负责向 column 实例添加方法。例如,在排序功能中,createColumn 方法添加了许多列实例 API 方法,例如 getNextSortingOrder、toggleSorting 等。因此,当你调用 column.toggleSorting() 时,你正在调用由 RowSorting 功能添加到列实例的方法。
createRow
表格功能中的 createRow 方法负责向 row 实例添加方法。例如,在行选择功能中,createRow 方法添加了许多行实例 API 方法,例如 toggleSelected、getIsSelected 等。因此,当你调用 row.toggleSelected() 时,你正在调用由 RowSelection 功能添加到行实例的方法。
createCell
表格功能中的 createCell 方法负责向 cell 实例添加方法。例如,在列分组功能中,createCell 方法添加了许多单元格实例 API 方法,例如 getIsGrouped、getIsAggregated 等。因此,当你调用 cell.getIsGrouped() 时,你正在调用由 ColumnGrouping 功能添加到单元格实例的方法。
添加自定义功能
让我们来制作一个用于假设用例的自定义表格功能。假设我们想向表格实例添加一个功能,允许用户更改表格的“密度”(单元格的内边距)。
查看完整的 custom-features 示例以查看完整实现,但这里是创建自定义功能的深入步骤。
第 1 步:设置 TypeScript 类型
假设你想要与 TanStack Table 中内置功能相同的完整类型安全,让我们为我们的新功能设置所有 TypeScript 类型。我们将为新的表格选项、状态和表格实例 API 方法创建类型。
这些类型遵循 TanStack Table 内部使用的命名约定,但你可以随意命名。我们还没有将这些类型添加到 TanStack Table,但我们将在下一步中这样做。
// 为我们新功能的自定义状态定义类型
export type DensityState = 'sm' | 'md' | 'lg';
export interface DensityTableState {
density: DensityState;
}
// 为我们新功能的表格选项定义类型
export interface DensityOptions {
enableDensity?: boolean;
onDensityChange?: OnChangeFn<DensityState>;
}
// 为我们新功能的表格 API 定义类型
export interface DensityInstance {
setDensity: (updater: Updater<DensityState>) => void;
toggleDensity: (value?: DensityState) => void;
}
第 2 步:使用声明合并将新类型添加到 TanStack Table
我们可以告诉 TypeScript 修改从 TanStack Table 导出的类型,以包含我们新功能的类型。这称为“声明合并”,它是 TypeScript 的一个强大功能。这样,我们就不必在我们的新功能代码或应用程序代码中使用任何 TypeScript 技巧,例如 as unknown as CustomTable 或 // @ts-ignore。
// 使用声明合并将我们新功能的 API 和状态类型添加到 TanStack Table 现有的类型中。
declare module '@tanstack/react-table' {
// 或你正在使用的任何框架适配器
// 合并我们新功能的状态与现有表格状态
interface TableState extends DensityTableState {}
// 合并我们新功能的选项与现有表格选项
interface TableOptionsResolved<TData extends RowData> extends DensityOptions {}
// 合并我们新功能的实例 API 与现有表格实例 API
interface Table<TData extends RowData> extends DensityInstance {}
// 如果你需要添加单元格实例 API...
// interface Cell<TData extends RowData, TValue> extends DensityCell
// 如果你需要添加行实例 API...
// interface Row<TData extends RowData> extends DensityRow
// 如果你需要添加列实例 API...
// interface Column<TData extends RowData, TValue> extends DensityColumn
// 如果你需要添加标题实例 API...
// interface Header<TData extends RowData, TValue> extends DensityHeader
// 注意:`ColumnDef` 无法进行声明合并,因为它是一个复杂类型,而不是接口。
// 但你仍然可以在 `ColumnDef.meta` 上使用声明合并。
}
一旦我们正确地完成了这一点,当我们尝试创建新功能代码并在应用程序中使用它时,应该不会有 TypeScript 错误。
使用声明合并的注意事项
使用声明合并的一个注意事项是,它会影响代码库中每个表格的 TanStack Table 类型。如果你打算为应用程序中的每个表格加载相同的功能集,这不是问题,但如果你的某些表格加载了额外的功能而有些没有,则可能会出现问题。或者,你可以只创建一堆自定义类型,这些类型从 TanStack Table 类型扩展,并添加了你的新功能。Material React Table 就是这样做的,以避免影响普通 TanStack Table 表格的类型,但这有点乏味,并且在某些地方需要大量的类型转换。
第 3 步:创建功能对象
完成了所有 TypeScript 设置后,我们现在可以为新功能创建功能对象。在这里,我们定义将添加到表格实例的所有方法。
使用 TableFeature 类型来确保你正确创建功能对象。如果 TypeScript 类型设置正确,那么当你使用新的状态、选项和实例 API 创建功能对象时,应该不会出现 TypeScript 错误。
export const DensityFeature: TableFeature<any> = {
// 使用 TableFeature 类型!!
// 定义新功能的初始状态
getInitialState: (state): DensityTableState => {
return {
density: 'md',
...state,
};
},
// 定义新功能的默认选项
getDefaultOptions: <TData extends RowData>(table: Table<TData>): DensityOptions => {
return {
enableDensity: true,
onDensityChange: makeStateUpdater('density', table),
} as DensityOptions;
},
// 如果你需要添加默认列定义...
// getDefaultColumnDef: <TData extends RowData>(): Partial<ColumnDef<TData>> => {
// return { meta: {} } // 使用 meta 而不是直接添加到 columnDef 以避免难以解决的 typescript 问题
// },
// 定义新功能的表格实例方法
createTable: <TData extends RowData>(table: Table<TData>): void => {
table.setDensity = (updater) => {
const safeUpdater: Updater<DensityState> = (old) => {
let newState = functionalUpdate(updater, old);
return newState;
};
return table.options.onDensityChange?.(safeUpdater);
};
table.toggleDensity = (value) => {
table.setDensity((old) => {
if (value) return value;
return old === 'lg' ? 'md' : old === 'md' ? 'sm' : 'lg'; // 在 3 个选项之间循环
});
};
},
// 如果你需要添加行实例 API...
// createRow: <TData extends RowData>(row, table): void => {},
// 如果你需要添加单元格实例 API...
// createCell: <TData extends RowData>(cell, column, row, table): void => {},
// 如果你需要添加列实例 API...
// createColumn: <TData extends RowData>(column, table): void => {},
// 如果你需要添加标题实例 API...
// createHeader: <TData extends RowData>(header, table): void => {},
};
第 4 步:将功能添加到表格
现在我们有了功能对象,我们可以在创建表格实例时将其传递给 _features 选项,从而将其添加到表格实例中。
const table = useReactTable({
_features: [DensityFeature], // 传入新功能以在底层与所有内置功能合并
columns,
data,
//..
});
第 5 步:在你的应用程序中使用该功能
现在该功能已添加到表格实例中,你可以在应用程序中使用新的实例 API 选项和状态。
const table = useReactTable({
_features: [DensityFeature], // 在创建时将我们的自定义功能传递给表格以进行实例化
columns,
data,
//...
state: {
density, // 将密度状态传递给表格,TS 仍然正常 :)
},
onDensityChange: setDensity, // 使用新的 onDensityChange 选项,TS 仍然正常 :)
});
//...
const { density } = table.getState();
return (
<td
key={cell.id}
style={{
// 在代码中使用我们的新功能
padding: density === 'sm' ? '4px' : density === 'md' ? '8px' : '16px',
transition: 'padding 0.2s',
}}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
);
我们必须这样做吗?
这只是一种将自定义代码与 TanStack Table 中内置功能集成在一起的新方法。在上面的示例中,我们本可以同样轻松地将 density 状态存储在 React.useState 中,在其他任何地方定义我们自己的 toggleDensity 处理程序,并将其与表格实例分开在我们的代码中使用。与 TanStack Table 一起构建表格功能而不是将其深入集成到表格实例中仍然是构建自定义功能的一种完全有效的方式。根据你的用例,这可能是也可能不是用自定义功能扩展 TanStack Table 的最简洁方式。