Möchten Sie direkt zur Implementierung springen? Sehen Sie sich diese Beispiele an
In diesem Leitfaden erfahren Sie, wie Sie TanStack Table mit benutzerdefinierten Features erweitern und lernen dabei mehr über die Struktur und Funktionsweise der TanStack Table v8 Codebasis.
TanStack Table verfügt über einen Kernsatz von Features, die in die Bibliothek integriert sind, wie z. B. Sortierung, Filterung, Paginierung usw. Wir haben viele Anfragen und manchmal auch gut durchdachte PRs erhalten, um weitere Features in die Bibliothek aufzunehmen. Obwohl wir stets offen für die Verbesserung der Bibliothek sind, möchten wir auch sicherstellen, dass TanStack Table eine schlanke Bibliothek bleibt, die nicht zu viel Ballast und Code enthält, der in den meisten Anwendungsfällen unwahrscheinlich verwendet wird. Nicht jeder PR kann oder sollte in die Kernbibliothek aufgenommen werden, auch wenn er ein echtes Problem löst. Dies kann für Entwickler frustrierend sein, deren Anwendungsfall TanStack Table zu 90 % abdeckt, sie aber etwas mehr Kontrolle benötigen.
TanStack Table war schon immer so aufgebaut, dass es hochgradig erweiterbar ist (zumindest seit v7). Die table-Instanz, die von jedem Framework-Adapter zurückgegeben wird, den Sie verwenden (useReactTable, useVueTable usw.), ist ein einfacher JavaScript-Objekt, dem zusätzliche Eigenschaften oder APIs hinzugefügt werden können. Es war schon immer möglich, Komposition zu verwenden, um benutzerdefinierte Logik, Zustände und APIs zur Tabelleninstanz hinzuzufügen. Bibliotheken wie Material React Table haben einfach benutzerdefinierte Wrapper-Hooks um den useReactTable-Hook erstellt, um die Tabelleninstanz mit benutzerdefinierten Funktionalitäten zu erweitern.
Ab Version 8.14.0 hat TanStack Table jedoch eine neue _features-Tabellenoption verfügbar gemacht, die es Ihnen ermöglicht, benutzerdefinierten Code enger und sauberer in die Tabelleninstanz zu integrieren, und zwar genau auf die gleiche Weise wie die integrierten Tabellen-Features.
TanStack Table v8.14.0 hat eine neue _features-Option eingeführt, die es Ihnen ermöglicht, benutzerdefinierte Features zur Tabelleninstanz hinzuzufügen.
Mit dieser neuen engeren Integration können Sie einfach komplexere benutzerdefinierte Features zu Ihren Tabellen hinzufügen und sie möglicherweise sogar bündeln und mit der Community teilen. Wir werden sehen, wie sich dies im Laufe der Zeit entwickelt. In einer zukünftigen v9-Version könnten wir sogar die Bundle-Größe von TanStack Table reduzieren, indem wir alle Features optional machen, aber das wird noch erforscht.
Der Quellcode von TanStack Table ist wohl eher einfach (zumindest finden wir das). Der gesamte Code für jedes Feature ist in einem eigenen Objekt/Datei aufgeteilt, mit Instanziierungsmethoden zur Erstellung von Anfangszuständen, Standard-Tabellen- und Spaltenoptionen sowie API-Methoden, die zu den table-, header-, column-, row- und cell-Instanzen hinzugefügt werden können.
Die gesamte Funktionalität eines Feature-Objekts kann durch den TableFeature-Typ beschrieben werden, der von TanStack Table exportiert wird. Dieser Typ ist eine TypeScript-Schnittstelle, die die Form eines Feature-Objekts beschreibt, das zur Erstellung eines Features benötigt wird.
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>
}
Das mag etwas verwirrend sein, lassen Sie uns also aufschlüsseln, was jede dieser Methoden tut
Die Methode getDefaultOptions in einem Tabellen-Feature ist für die Festlegung der Standard-Tabellenoptionen für dieses Feature verantwortlich. Zum Beispiel legt die Methode getDefaultOptions im Feature Column Sizing die Standardoption columnResizeMode mit dem Standardwert "onEnd" fest.
Die Methode getDefaultColumnDef in einem Tabellen-Feature ist für die Festlegung der Standard-Spaltenoptionen für dieses Feature verantwortlich. Zum Beispiel legt die Methode getDefaultColumnDef im Feature Sorting die Standard-Spaltenoption sortUndefined mit dem Standardwert 1 fest.
Die Methode getInitialState in einem Tabellen-Feature ist für die Festlegung des Standardzustands für dieses Feature verantwortlich. Zum Beispiel legt die Methode getInitialState im Feature Pagination den Standardzustand pageSize mit dem Wert 10 und den Standardzustand pageIndex mit dem Wert 0 fest.
Die Methode createTable in einem Tabellen-Feature ist für das Hinzufügen von Methoden zur table-Instanz verantwortlich. Zum Beispiel fügt die Methode createTable im Feature Row Selection viele API-Methoden für die Tabelleninstanz hinzu, wie z. B. toggleAllRowsSelected, getIsAllRowsSelected, getIsSomeRowsSelected usw. Wenn Sie also table.toggleAllRowsSelected() aufrufen, rufen Sie eine Methode auf, die von dem Feature RowSelection zur Tabelleninstanz hinzugefügt wurde.
Die Methode createHeader in einem Tabellen-Feature ist für das Hinzufügen von Methoden zur header-Instanz verantwortlich. Zum Beispiel fügt die Methode createHeader im Feature Column Sizing viele API-Methoden für die Header-Instanz hinzu, wie z. B. getStart und viele andere. Wenn Sie also header.getStart() aufrufen, rufen Sie eine Methode auf, die von dem Feature ColumnSizing zur Header-Instanz hinzugefügt wurde.
Die Methode createColumn in einem Tabellen-Feature ist für das Hinzufügen von Methoden zur column-Instanz verantwortlich. Zum Beispiel fügt die Methode createColumn im Feature Sorting viele API-Methoden für die Spalteninstanz hinzu, wie z. B. getNextSortingOrder, toggleSorting usw. Wenn Sie also column.toggleSorting() aufrufen, rufen Sie eine Methode auf, die von dem Feature RowSorting zur Spalteninstanz hinzugefügt wurde.
Die Methode createRow in einem Tabellen-Feature ist für das Hinzufügen von Methoden zur row-Instanz verantwortlich. Zum Beispiel fügt die Methode createRow im Feature Row Selection viele API-Methoden für die Zeileninstanz hinzu, wie z. B. toggleSelected, getIsSelected usw. Wenn Sie also row.toggleSelected() aufrufen, rufen Sie eine Methode auf, die von dem Feature RowSelection zur Zeileninstanz hinzugefügt wurde.
Die Methode createCell in einem Tabellen-Feature ist für das Hinzufügen von Methoden zur cell-Instanz verantwortlich. Zum Beispiel fügt die Methode createCell im Feature Column Grouping viele API-Methoden für die Zelleninstanz hinzu, wie z. B. getIsGrouped, getIsAggregated usw. Wenn Sie also cell.getIsGrouped() aufrufen, rufen Sie eine Methode auf, die von dem Feature ColumnGrouping zur Zelleninstanz hinzugefügt wurde.
Lassen Sie uns ein benutzerdefiniertes Tabellen-Feature für einen hypothetischen Anwendungsfall durchgehen. Nehmen wir an, wir möchten der Tabelleninstanz ein Feature hinzufügen, das es dem Benutzer ermöglicht, die "Dichte" (Abstand der Zellen) der Tabelle zu ändern.
Schauen Sie sich das vollständige custom-features-Beispiel an, um die vollständige Implementierung zu sehen, aber hier ist ein detaillierter Blick auf die Schritte zur Erstellung eines benutzerdefinierten Features.
Unter der Annahme, dass Sie die gleiche vollständige Typsicherheit wünschen wie die integrierten Features in TanStack Table, richten wir alle TypeScript-Typen für unser neues Feature ein. Wir erstellen Typen für neue Tabellenoptionen, Zustände und Tabelleninstanz-API-Methoden.
Diese Typen folgen der Namenskonvention, die intern in TanStack Table verwendet wird, aber Sie können sie benennen, wie Sie möchten. Wir fügen diese Typen TanStack Table noch nicht hinzu, das machen wir im nächsten Schritt.
// 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
}
Wir können TypeScript anweisen, die von TanStack Table exportierten Typen zu ändern, um unsere neuen Feature-Typen einzuschließen. Dies wird als "Declaration Merging" bezeichnet und ist ein mächtiges Feature von TypeScript. Auf diese Weise müssen wir keine TypeScript-Hacks wie as unknown as CustomTable oder // @ts-ignore in unserem neuen Feature-Code oder in unserem Anwendungscode verwenden.
// 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`
}
Wenn wir dies korrekt tun, sollten wir keine TypeScript-Fehler haben, wenn wir sowohl unseren neuen Feature-Code erstellen als auch ihn in unserer Anwendung verwenden.
Ein Vorbehalt bei der Verwendung von Declaration Merging ist, dass es sich auf die TanStack Table-Typen für jede Tabelle in Ihrer Codebasis auswirkt. Dies ist kein Problem, wenn Sie planen, für jede Tabelle in Ihrer Anwendung das gleiche Feature-Set zu laden, aber es könnte ein Problem sein, wenn einige Ihrer Tabellen zusätzliche Features laden und andere nicht. Alternativ können Sie einfach eine Reihe von benutzerdefinierten Typen erstellen, die von den TanStack Table-Typen mit Ihren neuen Features erweitert werden. Dies tut Material React Table, um die Typen von Vanilla TanStack Table-Tabellen nicht zu beeinträchtigen, ist aber etwas mühsamer und erfordert an bestimmten Stellen viel Typ-Casting.
Nachdem all diese TypeScript-Einrichtung abgeschlossen ist, können wir nun das Feature-Objekt für unser neues Feature erstellen. Hier definieren wir alle Methoden, die der Tabelleninstanz hinzugefügt werden.
Verwenden Sie den TableFeature-Typ, um sicherzustellen, dass Sie das Feature-Objekt korrekt erstellen. Wenn die TypeScript-Typen korrekt eingerichtet sind, sollten Sie keine TypeScript-Fehler haben, wenn Sie das Feature-Objekt mit dem neuen Zustand, den Optionen und den Instanz-APIs erstellen.
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 => {},
}
Jetzt, da wir unser Feature-Objekt haben, können wir es zur Tabelleninstanz hinzufügen, indem wir es der Option _features übergeben, wenn wir die Tabelleninstanz erstellen.
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,
//..
})
Nun, da das Feature zur Tabelleninstanz hinzugefügt wurde, können Sie die neuen Instanz-APIs, Optionen und Zustände in Ihrer Anwendung verwenden.
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>
)
Dies ist nur eine neue Möglichkeit, benutzerdefinierten Code parallel zu den integrierten Features in TanStack Table zu integrieren. In unserem obigen Beispiel hätten wir den density-Zustand auch in einem React.useState speichern, unseren eigenen toggleDensity-Handler definieren, wo auch immer, und ihn einfach separat von der Tabelleninstanz in unserem Code verwenden können. Das Erstellen von Tabellen-Features parallel zu TanStack Table anstatt sie tief in die Tabelleninstanz zu integrieren, ist immer noch ein vollkommen gültiger Weg, benutzerdefinierte Features zu erstellen. Je nach Ihrem Anwendungsfall ist dies möglicherweise nicht der sauberste Weg, TanStack Table mit benutzerdefinierten Features zu erweitern.
Ihre wöchentliche Dosis JavaScript-Nachrichten. Jeden Montag kostenlos an über 100.000 Entwickler geliefert.