想要跳过实现?查看这些示例
在本指南中,我们将介绍如何使用自定义功能扩展 TanStack Table,并在此过程中,我们将更多地了解 TanStack Table v8 代码库的结构及其工作原理。
TanStack Table 具有内置于库中的核心功能集,例如排序、过滤、分页等。我们收到了很多请求,有时甚至是一些经过深思熟虑的 PR,要求向库中添加更多功能。虽然我们始终乐于改进库,但我们也希望确保 TanStack Table 仍然是一个精简的库,不包含太多臃肿和不太可能在大多数用例中使用的代码。并非每个 PR 都可以或应该被核心库接受,即使它确实解决了实际问题。这可能会让开发人员感到沮丧,因为 TanStack Table 解决了他们 90% 的用例,但他们需要更多的控制权。
TanStack Table 一直以一种高度可扩展的方式构建(至少自 v7 以来)。从您正在使用的任何框架适配器(useReactTable、useVueTable 等)返回的 table 实例是一个普通的 JavaScript 对象,可以向其中添加额外的属性或 API。一直以来,都可以使用组合向表格实例添加自定义逻辑、状态和 API。像 Material React Table 这样的库只是围绕 useReactTable hook 创建了自定义包装器 hook,以使用自定义功能扩展表格实例。
但是,从 8.14.0 版本开始,TanStack Table 公开了一个新的 _features 表格选项,允许您以与内置表格功能已集成的方式完全相同的方式,更紧密、更干净地将自定义代码集成到表格实例中。
TanStack Table v8.14.0 引入了一个新的 _features 选项,允许您向表格实例添加自定义功能。
通过这种新的更紧密的集成,您可以轻松地向表格添加更复杂的自定义功能,甚至可以将它们打包起来并与社区分享。我们将观察这如何随着时间的推移而发展。在未来的 v9 版本中,我们甚至可能通过使所有功能都可选择加入来降低 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>
}
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 方法将默认的 columnResizeMode 选项设置为默认值 "onEnd"。
表格功能中的 getDefaultColumnDef 方法负责设置该功能的默认列选项。例如,在 排序 功能中,getDefaultColumnDef 方法将默认的 sortUndefined 列选项设置为默认值 1。
表格功能中的 getInitialState 方法负责设置该功能的默认状态。例如,在 分页 功能中,getInitialState 方法将默认的 pageSize 状态设置为值 10,并将默认的 pageIndex 状态设置为值 0。
表格功能中的 createTable 方法负责向 table 实例添加方法。例如,在 行选择 功能中,createTable 方法添加了许多表格实例 API 方法,例如 toggleAllRowsSelected、getIsAllRowsSelected、getIsSomeRowsSelected 等。因此,当您调用 table.toggleAllRowsSelected() 时,您正在调用由 RowSelection 功能添加到表格实例的方法。
表格功能中的 createHeader 方法负责向 header 实例添加方法。例如,在 列大小调整 功能中,createHeader 方法添加了许多表头实例 API 方法,例如 getStart 以及许多其他方法。因此,当您调用 header.getStart() 时,您正在调用由 ColumnSizing 功能添加到表头实例的方法。
表格功能中的 createColumn 方法负责向 column 实例添加方法。例如,在 排序 功能中,createColumn 方法添加了许多列实例 API 方法,例如 getNextSortingOrder、toggleSorting 等。因此,当您调用 column.toggleSorting() 时,您正在调用由 RowSorting 功能添加到列实例的方法。
表格功能中的 createRow 方法负责向 row 实例添加方法。例如,在 行选择 功能中,createRow 方法添加了许多行实例 API 方法,例如 toggleSelected、getIsSelected 等。因此,当您调用 row.toggleSelected() 时,您正在调用由 RowSelection 功能添加到行实例的方法。
表格功能中的 createCell 方法负责向 cell 实例添加方法。例如,在 列分组 功能中,createCell 方法添加了许多单元格实例 API 方法,例如 getIsGrouped、getIsAggregated 等。因此,当您调用 cell.getIsGrouped() 时,您正在调用由 ColumnGrouping 功能添加到单元格实例的方法。
让我们逐步完成为假设用例创建自定义表格功能的过程。假设我们想向表格实例添加一个功能,允许用户更改表格的“密度”(单元格的内边距)。
查看完整的 custom-features 示例以查看完整实现,但这里深入了解创建自定义功能的步骤。
假设您想要与 TanStack Table 中的内置功能相同的完整类型安全性,让我们为新功能设置所有 TypeScript 类型。我们将为新的表格选项、状态和表格实例 API 方法创建类型。
这些类型遵循 TanStack Table 内部使用的命名约定,但您可以随意命名它们。我们尚未将这些类型添加到 TanStack Table 中,但我们将在下一步中执行此操作。
// define types for our new feature's custom state
export type DensityState = 'sm' | 'md' | 'lg'
export interface DensityTableState {
density: DensityState
}
// define types for our new feature's table options
export interface DensityOptions {
enableDensity?: boolean
onDensityChange?: OnChangeFn<DensityState>
}
// Define types for our new feature's table APIs
export interface DensityInstance {
setDensity: (updater: Updater<DensityState>) => void
toggleDensity: (value?: DensityState) => void
}
// define types for our new feature's custom state
export type DensityState = 'sm' | 'md' | 'lg'
export interface DensityTableState {
density: DensityState
}
// define types for our new feature's table options
export interface DensityOptions {
enableDensity?: boolean
onDensityChange?: OnChangeFn<DensityState>
}
// Define types for our new feature's table APIs
export interface DensityInstance {
setDensity: (updater: Updater<DensityState>) => void
toggleDensity: (value?: DensityState) => void
}
我们可以告诉 TypeScript 修改从 TanStack Table 导出的类型,以包含我们新功能的类型。这称为“声明合并”,它是 TypeScript 的一项强大功能。这样,我们就不必在新功能的代码或应用程序代码中使用任何 TypeScript hack,例如 as unknown as CustomTable 或 // @ts-ignore。
// Use declaration merging to add our new feature APIs and state types to TanStack Table's existing types.
declare module '@tanstack/react-table' { // or whatever framework adapter you are using
//merge our new feature's state with the existing table state
interface TableState extends DensityTableState {}
//merge our new feature's options with the existing table options
interface TableOptionsResolved<TData extends RowData>
extends DensityOptions {}
//merge our new feature's instance APIs with the existing table instance APIs
interface Table<TData extends RowData> extends DensityInstance {}
// if you need to add cell instance APIs...
// interface Cell<TData extends RowData, TValue> extends DensityCell
// if you need to add row instance APIs...
// interface Row<TData extends RowData> extends DensityRow
// if you need to add column instance APIs...
// interface Column<TData extends RowData, TValue> extends DensityColumn
// if you need to add header instance APIs...
// interface Header<TData extends RowData, TValue> extends DensityHeader
// Note: declaration merging on `ColumnDef` is not possible because it is a complex type, not an interface.
// But you can still use declaration merging on `ColumnDef.meta`
}
// Use declaration merging to add our new feature APIs and state types to TanStack Table's existing types.
declare module '@tanstack/react-table' { // or whatever framework adapter you are using
//merge our new feature's state with the existing table state
interface TableState extends DensityTableState {}
//merge our new feature's options with the existing table options
interface TableOptionsResolved<TData extends RowData>
extends DensityOptions {}
//merge our new feature's instance APIs with the existing table instance APIs
interface Table<TData extends RowData> extends DensityInstance {}
// if you need to add cell instance APIs...
// interface Cell<TData extends RowData, TValue> extends DensityCell
// if you need to add row instance APIs...
// interface Row<TData extends RowData> extends DensityRow
// if you need to add column instance APIs...
// interface Column<TData extends RowData, TValue> extends DensityColumn
// if you need to add header instance APIs...
// interface Header<TData extends RowData, TValue> extends DensityHeader
// Note: declaration merging on `ColumnDef` is not possible because it is a complex type, not an interface.
// But you can still use declaration merging on `ColumnDef.meta`
}
一旦我们正确地执行此操作,当我们尝试创建新功能的代码并在我们的应用程序中使用它时,我们应该不会遇到 TypeScript 错误。
使用声明合并的一个注意事项是,它将影响代码库中每个表格的 TanStack Table 类型。如果您计划为应用程序中的每个表格加载相同的功能集,则这不是问题,但如果您的某些表格加载了额外的功能,而另一些表格没有,则可能会出现问题。或者,您可以只创建一堆自定义类型,这些类型使用添加的新功能扩展了 TanStack Table 类型。这就是 Material React Table 为了避免影响 vanilla TanStack Table 表格的类型而执行的操作,但这有点繁琐,并且在某些时候需要进行大量类型转换。
完成了所有 TypeScript 设置之后,我们现在可以为我们的新功能创建功能对象。这是我们定义将添加到表格实例的所有方法的地方。
使用 TableFeature 类型以确保您正确创建功能对象。如果 TypeScript 类型设置正确,则在创建具有新状态、选项和实例 API 的功能对象时,您应该不会遇到 TypeScript 错误。
export const DensityFeature: TableFeature<any> = { //Use the TableFeature type!!
// define the new feature's initial state
getInitialState: (state): DensityTableState => {
return {
density: 'md',
...state,
}
},
// define the new feature's default options
getDefaultOptions: <TData extends RowData>(
table: Table<TData>
): DensityOptions => {
return {
enableDensity: true,
onDensityChange: makeStateUpdater('density', table),
} as DensityOptions
},
// if you need to add a default column definition...
// getDefaultColumnDef: <TData extends RowData>(): Partial<ColumnDef<TData>> => {
// return { meta: {} } //use meta instead of directly adding to the columnDef to avoid typescript stuff that's hard to workaround
// },
// define the new feature's table instance methods
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' //cycle through the 3 options
})
}
},
// if you need to add row instance APIs...
// createRow: <TData extends RowData>(row, table): void => {},
// if you need to add cell instance APIs...
// createCell: <TData extends RowData>(cell, column, row, table): void => {},
// if you need to add column instance APIs...
// createColumn: <TData extends RowData>(column, table): void => {},
// if you need to add header instance APIs...
// createHeader: <TData extends RowData>(header, table): void => {},
}
export const DensityFeature: TableFeature<any> = { //Use the TableFeature type!!
// define the new feature's initial state
getInitialState: (state): DensityTableState => {
return {
density: 'md',
...state,
}
},
// define the new feature's default options
getDefaultOptions: <TData extends RowData>(
table: Table<TData>
): DensityOptions => {
return {
enableDensity: true,
onDensityChange: makeStateUpdater('density', table),
} as DensityOptions
},
// if you need to add a default column definition...
// getDefaultColumnDef: <TData extends RowData>(): Partial<ColumnDef<TData>> => {
// return { meta: {} } //use meta instead of directly adding to the columnDef to avoid typescript stuff that's hard to workaround
// },
// define the new feature's table instance methods
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' //cycle through the 3 options
})
}
},
// if you need to add row instance APIs...
// createRow: <TData extends RowData>(row, table): void => {},
// if you need to add cell instance APIs...
// createCell: <TData extends RowData>(cell, column, row, table): void => {},
// if you need to add column instance APIs...
// createColumn: <TData extends RowData>(column, table): void => {},
// if you need to add header instance APIs...
// createHeader: <TData extends RowData>(header, table): void => {},
}
现在我们有了功能对象,我们可以通过在创建表格实例时将其传递给 _features 选项,将其添加到表格实例。
const table = useReactTable({
_features: [DensityFeature], //pass the new feature to merge with all of the built-in features under the hood
columns,
data,
//..
})
const table = useReactTable({
_features: [DensityFeature], //pass the new feature to merge with all of the built-in features under the hood
columns,
data,
//..
})
现在该功能已添加到表格实例,您可以在应用程序中使用新的实例 API 选项和状态。
const table = useReactTable({
_features: [DensityFeature], //pass our custom feature to the table to be instantiated upon creation
columns,
data,
//...
state: {
density, //passing the density state to the table, TS is still happy :)
},
onDensityChange: setDensity, //using the new onDensityChange option, TS is still happy :)
})
//...
const { density } = table.getState()
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',
}}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</td>
)
const table = useReactTable({
_features: [DensityFeature], //pass our custom feature to the table to be instantiated upon creation
columns,
data,
//...
state: {
density, //passing the density state to the table, TS is still happy :)
},
onDensityChange: setDensity, //using the new onDensityChange option, TS is still happy :)
})
//...
const { density } = table.getState()
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',
}}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</td>
)
这只是一种将自定义代码与 TanStack Table 中的内置功能一起集成的新方法。在我们上面的示例中,我们可以很容易地将 density 状态存储在 React.useState 中,在任何地方定义我们自己的 toggleDensity 处理程序,并将其与表格实例分开在我们的代码中使用。与 TanStack Table 并行构建表格功能,而不是将其深入集成到表格实例中,仍然是构建自定义功能的完全有效的方法。根据您的用例,这可能不是或可能是使用自定义功能扩展 TanStack Table 的最简洁方法。
您的每周 JavaScript 新闻。每周一免费发送给超过 100,000 名开发人员。