框架
版本
企业版

自定义功能指南

示例

想直接看实现?可以参考以下示例

自定义功能指南

在本指南中,我们将介绍如何使用自定义功能扩展 TanStack Table,在此过程中,我们将更深入地了解 TanStack Table v8 代码库的结构及其工作原理。

TanStack Table 力求精简

TanStack Table 具有一些内置的核心功能,例如排序、过滤、分页等。我们收到了大量关于添加更多功能的请求,有时甚至还有一些深思熟虑的 PR。虽然我们始终乐于改进库,但我们也希望确保 TanStack Table 保持精简,不包含过多的臃肿代码,以及在大多数用例中不太可能使用到的代码。并非所有 PR 都能(或应该)被合并到核心库中,即使它们确实解决了实际问题。这可能会让那些 TanStack Table 满足了他们 90% 的用例但还需要更多控制权的开发者感到沮丧。

TanStack Table 一直以来(至少自 v7 版本以来)的设计都允许其高度可扩展。您使用的框架适配器(如 useReactTableuseVueTable 等)返回的 table 实例是一个普通的 JavaScript 对象,可以向其添加额外的属性或 API。通过组合来为 table 实例添加自定义逻辑、状态和 API 始终是可能的。像 Material React Table 这样的库,只是围绕 useReactTable hook 创建了自定义包装器 hook,以扩展 table 实例的功能。

然而,从 8.14.0 版本开始,TanStack Table 公开了一个新的 _features table 选项,该选项允许您以与内置 table 功能相同的方式,更紧密、更干净地将自定义代码集成到 table 实例中。

TanStack Table v8.14.0 引入了一个新的 _features 选项,允许您将自定义功能添加到 table 实例。

通过这种更紧密的集成,您可以轻松地为您的表格添加更复杂的功能,甚至可以打包它们与社区分享。我们将随着时间的推移观察它的发展。在未来的 v9 版本中,我们甚至可能通过使所有功能成为可选来减小 TanStack Table 的包大小,但这仍在探索中。

TanStack Table 的功能如何运作

TanStack Table 的源代码可以说是相当简单(至少我们是这么认为的)。每个功能的代码都被分割到自己的对象/文件中,包含用于创建初始状态、默认表格和列选项以及可以添加到 tableheadercolumnrowcell 实例的实例化方法。

功能对象的所有功能都可以用从 TanStack Table 导出的 TableFeature 类型来描述。这个类型是一个 TypeScript 接口,描述了创建功能所需的功能对象的形状。

ts
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

在 table 功能中的 getDefaultOptions 方法负责为该功能设置默认的 table 选项。例如,在 Column Sizing 功能中,getDefaultOptions 方法设置了默认的 columnResizeMode 选项,默认值为 "onEnd"


getDefaultColumnDef

在 table 功能中的 getDefaultColumnDef 方法负责为该功能设置默认的 column 选项。例如,在 Sorting 功能中,getDefaultColumnDef 方法设置了默认的 sortUndefined 列选项,默认值为 1


getInitialState

在 table 功能中的 getInitialState 方法负责为该功能设置默认状态。例如,在 Pagination 功能中,getInitialState 方法将默认的 pageSize 状态设置为 10,并将默认的 pageIndex 状态设置为 0

API 创建者


createTable

在 table 功能中的 createTable 方法负责向 table 实例添加方法。例如,在 Row Selection 功能中,createTable 方法添加了许多 table 实例 API 方法,如 toggleAllRowsSelectedgetIsAllRowsSelectedgetIsSomeRowsSelected 等。因此,当您调用 table.toggleAllRowsSelected() 时,您调用的是由 RowSelection 功能添加到 table 实例的方法。


createHeader

在 table 功能中的 createHeader 方法负责向 header 实例添加方法。例如,在 Column Sizing 功能中,createHeader 方法添加了许多 header 实例 API 方法,如 getStart 以及许多其他方法。因此,当您调用 header.getStart() 时,您调用的是由 ColumnSizing 功能添加到 header 实例的方法。


createColumn

在 table 功能中的 createColumn 方法负责向 column 实例添加方法。例如,在 Sorting 功能中,createColumn 方法添加了许多 column 实例 API 方法,如 getNextSortingOrdertoggleSorting 等。因此,当您调用 column.toggleSorting() 时,您调用的是由 RowSorting 功能添加到 column 实例的方法。


createRow

在 table 功能中的 createRow 方法负责向 row 实例添加方法。例如,在 Row Selection 功能中,createRow 方法添加了许多 row 实例 API 方法,如 toggleSelectedgetIsSelected 等。因此,当您调用 row.toggleSelected() 时,您调用的是由 RowSelection 功能添加到 row 实例的方法。


createCell

在 table 功能中的 createCell 方法负责向 cell 实例添加方法。例如,在 Column Grouping 功能中,createCell 方法添加了许多 cell 实例 API 方法,如 getIsGroupedgetIsAggregated 等。因此,当您调用 cell.getIsGrouped() 时,您调用的是由 ColumnGrouping 功能添加到 cell 实例的方法。

添加自定义功能

让我们通过为假设的用例创建一个自定义 table 功能来演示。假设我们要向 table 实例添加一个功能,允许用户更改 table 的“密度”(单元格的内边距)。

请查看完整的 custom-features 示例,了解完整实现,但这里将深入介绍创建自定义功能的步骤。

步骤 1:设置 TypeScript 类型

假设您希望获得与 TanStack Table 中内置功能相同的完整类型安全,那么让我们为新功能设置所有 TypeScript 类型。我们将为新的 table 选项、状态和 table 实例 API 方法创建类型。

这些类型遵循 TanStack Table 内部使用的命名约定,但您可以根据自己的喜好命名它们。我们还没有将这些类型添加到 TanStack Table,但将在下一步进行。

ts
// 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
}

步骤 2:使用声明合并为 TanStack Table 添加新类型

我们可以告诉 TypeScript 修改从 TanStack Table 导出的类型以包含我们新功能的类型。这被称为“声明合并”,它是 TypeScript 的一项强大功能。这样,我们就无需在我们的新功能代码或应用程序代码中使用任何 TypeScript 技巧,例如 as unknown as CustomTable// @ts-ignore

ts
// 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 错误。

使用声明合并的注意事项

使用声明合并的一个注意事项是,它会影响代码库中每个 table 的 TanStack Table 类型。如果您计划为应用程序中的每个 table 加载相同的功能集,这不会有问题,但如果某些 table 加载了额外功能而另一些则没有,这可能会成为一个问题。或者,您可以创建许多自定义类型,它们扩展自 TanStack Table 类型,并添加新功能。这就是 Material React Table 所做的,以避免影响 vanilla TanStack Table 的类型,但这有点乏味,并且在某些点需要大量类型转换。

步骤 3:创建功能对象

完成所有这些 TypeScript 设置后,我们就可以创建新功能的功能对象了。在这里,我们定义了将添加到 table 实例的所有方法。

使用 TableFeature 类型来确保您正确创建功能对象。如果 TypeScript 类型设置正确,当您使用新状态、选项和实例 API 创建功能对象时,应该不会出现 TypeScript 错误。

ts
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 => {},
}

步骤 4:将功能添加到表格

现在我们有了功能对象,可以通过在创建 table 实例时将其传递给 _features 选项来将其添加到 table 实例。

ts
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,
  //..
})

步骤 5:在您的应用程序中使用该功能

现在功能已添加到 table 实例,您可以在应用程序中使用新的实例 API 选项和状态。

tsx
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 处理程序,并在 table 实例之外单独使用它。将 table 功能与 TanStack Table 一起构建,而不是将其深度集成到 table 实例中,仍然是构建自定义功能的有效方法。根据您的用例,这可能是扩展 TanStack Table 自定义功能的最佳方法,也可能不是。

我们的合作伙伴
Code Rabbit
AG Grid
订阅 Bytes

您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。

Bytes

无垃圾邮件。您可以随时取消订阅。

订阅 Bytes

您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。

Bytes

无垃圾邮件。您可以随时取消订阅。