Skip to main content

Guía de Características Personalizadas

Ejemplos

¿Quieres saltar a la implementación? Echa un vistazo a estos ejemplos:

Guía de Características Personalizadas

En esta guía, cubriremos cómo extender TanStack Table con características personalizadas, y en el camino, aprenderemos más sobre cómo está estructurado y funciona el código base de TanStack Table v8.

TanStack Table Se Esfuerza por ser Ligera

TanStack Table tiene un conjunto central de características que están integradas en la biblioteca, como clasificación (sorting), filtrado (filtering), paginación (pagination), etc. Hemos recibido muchas solicitudes y, a veces, incluso algunos PRs bien pensados para añadir aún más características a la biblioteca. Si bien siempre estamos abiertos a mejorar la biblioteca, también queremos asegurarnos de que TanStack Table siga siendo una biblioteca ligera que no incluya demasiado bloat y código que es poco probable que se utilice en la mayoría de los casos de uso. No todos los PRs pueden, o deben, ser aceptados en la biblioteca central, incluso si resuelven un problema real. Esto puede ser frustrante para los desarrolladores cuando TanStack Table resuelve el 90% de su caso de uso, pero necesitan un poco más de control.

TanStack Table siempre se ha construido de una manera que le permite ser altamente extensible (al menos desde v7). La instancia table que se devuelve desde el framework adapter que estés utilizando (useReactTable, useVueTable, etc.) es un objeto JavaScript plano al que se le pueden añadir propiedades o APIs adicionales. Siempre ha sido posible usar la composición para añadir lógica, estado y APIs personalizadas a la instancia de la tabla. Bibliotecas como Material React Table simplemente han creado hooks envolventes personalizados alrededor del hook useReactTable para extender la instancia de la tabla con funcionalidad personalizada.

Sin embargo, a partir de la versión 8.14.0, TanStack Table ha expuesto una nueva opción de tabla _features que permite integrar código personalizado de forma más ajustada y limpia en la instancia de la tabla, exactamente de la misma manera en que ya están integradas las características de tabla integradas.

TanStack Table v8.14.0 introdujo una nueva opción _features que permite añadir características personalizadas a la instancia de la tabla.

Con esta nueva integración más ajustada, puedes añadir fácilmente características personalizadas más complejas a tus tablas, e incluso empaquetarlas y compartirlas con la comunidad. Veremos cómo evoluciona esto con el tiempo. En una futura versión v9, incluso podríamos reducir el bundle size de TanStack Table haciendo que todas las características sean opt-in, pero eso todavía se está explorando.

Cómo Funcionan las Características de TanStack Table

El código fuente de TanStack Table es posiblemente algo simple (al menos eso creemos). Todo el código para cada característica se divide en su propio objeto/archivo con métodos de instanciación para crear el estado inicial, las opciones predeterminadas de tabla y columna, y los métodos API que se pueden añadir a las instancias table, header, column, row y cell.

Toda la funcionalidad de un objeto de característica puede describirse con el tipo TableFeature que se exporta desde TanStack Table. Este tipo es una interfaz TypeScript que describe la forma de un objeto de característica necesario para crear una característica.

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>;
}

Esto podría ser un poco confuso, así que vamos a desglosar lo que hace cada uno de estos métodos:

Opciones Predeterminadas y Estado Inicial


getDefaultOptions

El método getDefaultOptions en una característica de tabla es responsable de establecer las opciones predeterminadas de la tabla para esa característica. Por ejemplo, en la característica Column Sizing, el método getDefaultOptions establece la opción columnResizeMode predeterminada con un valor predeterminado de "onEnd".


getDefaultColumnDef

El método getDefaultColumnDef en una característica de tabla es responsable de establecer las opciones predeterminadas de columna para esa característica. Por ejemplo, en la característica Sorting, el método getDefaultColumnDef establece la opción de columna sortUndefined predeterminada con un valor predeterminado de 1.


getInitialState

El método getInitialState en una característica de tabla es responsable de establecer el estado predeterminado para esa característica. Por ejemplo, en la característica Pagination, el método getInitialState establece el estado pageSize predeterminado con un valor de 10 y el estado pageIndex predeterminado con un valor de 0.

Creadores de API


createTable

El método createTable en una característica de tabla es responsable de añadir métodos a la instancia table. Por ejemplo, en la característica Row Selection, el método createTable añade muchos métodos API a la instancia de tabla, como toggleAllRowsSelected, getIsAllRowsSelected, getIsSomeRowsSelected, etc. Entonces, cuando llamas a table.toggleAllRowsSelected(), estás llamando a un método que fue añadido a la instancia de tabla por la característica RowSelection.


createHeader

El método createHeader en una característica de tabla es responsable de añadir métodos a la instancia header. Por ejemplo, en la característica Column Sizing, el método createHeader añade muchos métodos API a la instancia de encabezado, como getStart, y muchos otros. Entonces, cuando llamas a header.getStart(), estás llamando a un método que fue añadido a la instancia de encabezado por la característica ColumnSizing.


createColumn

El método createColumn en una característica de tabla es responsable de añadir métodos a la instancia column. Por ejemplo, en la característica Sorting, el método createColumn añade muchos métodos API a la instancia de columna, como getNextSortingOrder, toggleSorting, etc. Entonces, cuando llamas a column.toggleSorting(), estás llamando a un método que fue añadido a la instancia de columna por la característica RowSorting.


createRow

El método createRow en una característica de tabla es responsable de añadir métodos a la instancia row. Por ejemplo, en la característica Row Selection, el método createRow añade muchos métodos API a la instancia de fila, como toggleSelected, getIsSelected, etc. Entonces, cuando llamas a row.toggleSelected(), estás llamando a un método que fue añadido a la instancia de fila por la característica RowSelection.


createCell

El método createCell en una característica de tabla es responsable de añadir métodos a la instancia cell. Por ejemplo, en la característica Column Grouping, el método createCell añade muchos métodos API a la instancia de celda, como getIsGrouped, getIsAggregated, etc. Entonces, cuando llamas a cell.getIsGrouped(), estás llamando a un método que fue añadido a la instancia de celda por la característica ColumnGrouping.

Añadiendo una Característica Personalizada

Recorramos la creación de una característica de tabla personalizada para un caso de uso hipotético. Digamos que queremos añadir una característica a la instancia de la tabla que permita al usuario cambiar la "densidad" (relleno de las celdas) de la tabla.

Echa un vistazo al ejemplo completo de custom-features para ver la implementación completa, pero aquí tienes un vistazo en profundidad a los pasos para crear una característica personalizada.

Paso 1: Configurar los Tipos de TypeScript

Asumiendo que quieres la misma seguridad de tipos completa que tienen las características integradas en TanStack Table, vamos a configurar todos los tipos de TypeScript para nuestra nueva característica. Crearemos tipos para nuevas opciones de tabla, estado y métodos API de la instancia de tabla.

Estos tipos siguen la convención de nomenclatura utilizada internamente en TanStack Table, pero puedes nombrarlos como quieras. Todavía no estamos añadiendo estos tipos a TanStack Table, pero lo haremos en el siguiente paso.

// definir tipos para el estado personalizado de nuestra nueva característica
export type DensityState = "sm" | "md" | "lg";
export interface DensityTableState {
density: DensityState;
}

// definir tipos para las opciones de tabla de nuestra nueva característica
export interface DensityOptions {
enableDensity?: boolean;
onDensityChange?: OnChangeFn<DensityState>;
}

// Definir tipos para las APIs de tabla de nuestra nueva característica
export interface DensityInstance {
setDensity: (updater: Updater<DensityState>) => void;
toggleDensity: (value?: DensityState) => void;
}

Paso 2: Usar la Fusión de Declaraciones para Añadir Nuevos Tipos a TanStack Table

Podemos indicar a TypeScript que modifique los tipos exportados de TanStack Table para incluir los tipos de nuestra nueva característica. Esto se llama "declaration merging" (fusión de declaraciones) y es una característica potente de TypeScript. De esta manera, no deberíamos tener que usar ningún truco de TypeScript como as unknown as CustomTable o // @ts-ignore en el código de nuestra nueva característica o en el código de nuestra aplicación.

// Usar la fusión de declaraciones para añadir nuestras nuevas APIs de características y tipos de estado a los tipos existentes de TanStack Table.
declare module "@tanstack/react-table" {
// o cualquier framework adapter que estés usando
// fusionar el estado de nuestra nueva característica con el estado existente de la tabla
interface TableState extends DensityTableState {}
// fusionar las opciones de nuestra nueva característica con las opciones existentes de la tabla
interface TableOptionsResolved<
TData extends RowData,
> extends DensityOptions {}
// fusionar las APIs de instancia de nuestra nueva característica con las APIs de instancia de tabla existentes
interface Table<TData extends RowData> extends DensityInstance {}
// si necesitas añadir APIs de instancia de celda...
// interface Cell<TData extends RowData, TValue> extends DensityCell
// si necesitas añadir APIs de instancia de fila...
// interface Row<TData extends RowData> extends DensityRow
// si necesitas añadir APIs de instancia de columna...
// interface Column<TData extends RowData, TValue> extends DensityColumn
// si necesitas añadir APIs de instancia de encabezado...
// interface Header<TData extends RowData, TValue> extends DensityHeader

// Nota: la fusión de declaraciones en `ColumnDef` no es posible porque es un tipo complejo, no una interfaz.
// Pero aún puedes usar la fusión de declaraciones en `ColumnDef.meta`
}

Una vez que hagamos esto correctamente, no deberíamos tener errores de TypeScript cuando intentemos tanto crear el código de nuestra nueva característica como usarlo en nuestra aplicación.

Advertencias al Usar la Fusión de Declaraciones

Una advertencia al usar la fusión de declaraciones es que afectará los tipos de TanStack Table para cada tabla en tu código base. Esto no es un problema si planeas cargar el mismo conjunto de características para cada tabla en tu aplicación, pero podría ser un problema si algunas de tus tablas cargan características adicionales y otras no. Alternativamente, puedes simplemente crear un montón de tipos personalizados que extiendan los tipos de TanStack Table con tus nuevas características añadidas. Esto es lo que hace Material React Table para evitar afectar los tipos de las tablas TanStack Table vanilla, pero es un poco más tedioso y requiere mucha coerción de tipos en ciertos puntos.

Paso 3: Crear el Objeto de la Característica

Con toda esa configuración de TypeScript lista, ahora podemos crear el objeto de la característica para nuestra nueva característica. Aquí es donde definimos todos los métodos que se añadirán a la instancia de la tabla.

Usa el tipo TableFeature para asegurarte de que estás creando el objeto de la característica correctamente. Si los tipos de TypeScript están configurados correctamente, no deberías tener errores de TypeScript al crear el objeto de la característica con el nuevo estado, las opciones y las APIs de instancia.

export const DensityFeature: TableFeature<any> = {
// ¡Usa el tipo TableFeature!
// definir el estado inicial de la nueva característica
getInitialState: (state): DensityTableState => {
return {
density: "md",
...state,
};
},

// definir las opciones predeterminadas de la nueva característica
getDefaultOptions: <TData extends RowData>(
table: Table<TData>,
): DensityOptions => {
return {
enableDensity: true,
onDensityChange: makeStateUpdater("density", table),
} as DensityOptions;
},
// si necesitas añadir una definición de columna predeterminada...
// getDefaultColumnDef: <TData extends RowData>(): Partial<ColumnDef<TData>> => {
// return { meta: {} } // usa meta en lugar de añadir directamente a columnDef para evitar cosas de typescript difíciles de solucionar
// },

// definir los métodos de instancia de tabla de la nueva característica
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"; // ciclar a través de las 3 opciones
});
};
},

// si necesitas añadir APIs de instancia de fila...
// createRow: <TData extends RowData>(row, table): void => {},
// si necesitas añadir APIs de instancia de celda...
// createCell: <TData extends RowData>(cell, column, row, table): void => {},
// si necesitas añadir APIs de instancia de columna...
// createColumn: <TData extends RowData>(column, table): void => {},
// si necesitas añadir APIs de instancia de encabezado...
// createHeader: <TData extends RowData>(header, table): void => {},
};

Paso 4: Añadir la Característica a la Tabla

Ahora que tenemos nuestro objeto de característica, podemos añadirlo a la instancia de la tabla pasándolo a la opción _features cuando creamos la instancia de la tabla.

const table = useReactTable({
_features: [DensityFeature], // pasar la nueva característica para fusionar con todas las características integradas de forma interna
columns,
data,
//..
});

Paso 5: Usar la Característica en tu Aplicación

Ahora que la característica se ha añadido a la instancia de la tabla, puedes usar las nuevas opciones de APIs de instancia y el estado en tu aplicación.

const table = useReactTable({
_features: [DensityFeature], // pasar nuestra característica personalizada a la tabla para que sea instanciada al crearse
columns,
data,
//...
state: {
density, // pasando el estado de densidad a la tabla, TS sigue contento :)
},
onDensityChange: setDensity, // usando la nueva opción onDensityChange, TS sigue contento :)
});
//...
const { density } = table.getState();
return (
<td
key={cell.id}
style={{
// usando nuestra nueva característica en el código
padding: density === "sm" ? "4px" : density === "md" ? "8px" : "16px",
transition: "padding 0.2s",
}}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
);

¿Tenemos que Hacerlo de Esta Manera?

Esta es solo una nueva forma de integrar código personalizado junto con las características integradas en TanStack Table. En nuestro ejemplo anterior, podríamos haber almacenado el estado density en un React.useState con la misma facilidad, definido nuestro propio manejador toggleDensity donde sea, y simplemente usarlo en nuestro código por separado de la instancia de la tabla. Construir características de tabla junto a TanStack Table en lugar de integrarlas profundamente en la instancia de la tabla sigue siendo una forma perfectamente válida de construir características personalizadas. Dependiendo de tu caso de uso, esta puede o no ser la forma más limpia de extender TanStack Table con características personalizadas.