-
Notifications
You must be signed in to change notification settings - Fork 12
React DataTable
The datatable, based on Tanstack API, has multiple case of uses on which we must pay attention.
As Tanstack triggers a re-render each time some property passed to its API, we need to be sure that columns don't change on every render. For this reason, it's required to wrap columns definition in a useMemo hook.
const columns = useMemo(() => [
columHelper.display(),
columHelper.display(),
], [deps])
If columnHelper is provided as dependency of the useMemo, the columns could start trigger a re-render each time this function change. For this reason, it's important that the columns do not depend from the columnHelper. In order to achieve this, columnHelper can be declared outside the component, inside the useMemo hook or removed from the dependency array of the useMemo suppressing the eslint.
const columnHelper = createReactDataTableColumnHelper<MyType>();
const MyComponent = () => {
const columns = useMemo(() => [
columnHelper.display(),
columnHelper.accessor(),
...
], [otherDeps]);
return <ReactDataTable ..>
}
TanStack offers the possibility to manage table data both server side or client side.
By default, the data management is performed client side.
This behaviour can be overriden setting to true one or all between following parameters:
- manualPagination
- manualSorting
- manualFiltering
Data must be stable and downloading only once before being passed to the table. Filtering, sorting and pagination will be handled directly by Tanstack API. This doesn't mean that states can't be manually edited. So, state management can be entirely left to the package or externally managed declaring state and passing it to the table.
const columnHelper = createReactDataTableColumnHelper<MyType>();
const MyComponent = (props: { data: MyType[] }) => {
const { data } = props;
const columns = useMemo(() => [
columnHelper.display({
...
enableColumnFilter: true, // will be managed by Tankstack
...
}),
columnHelper.accessor(
...
enableSorting: true, // will be managed by Tankstack
...
),
...
], [otherDeps]);
const { table } = useReactDataTable<MyType, MyFilter>({
columns,
data,
state,
reactTableOptions: {
enableSortingRemoval: false,
},
});
return (
<ReactDataTable totalRecords={data.length} showPaging>
)
}
Server side magement is better in terms of performance but require more attention by the developer.
When should I enable manualSorting, manualPagination and manualFiltering?
When the related state is passed as query parameter.
If, for example, you manage columnFilters and pagination, but no fields on your table are sortable, there is no need to set manualSorting to true.
In case of server side pagination, the following code snippet should be respected.
Notice that setting manualPagination to true will disable all side effects provided by Tanstack (autoResetPageIndex, etc) as we will see later.
const { pagination, setPagination} = useReactDataTableState<MyType, MyFilter>({ initialPagination: { pageSize: 25 });
const { data, isFetching, refetch } = useMyTypeQuery({
filter: { ... },
options: {
limit: pagination.pageSize,
page: pagination.pageIndex + 1,
},
});
const columns = useMemo(() => [...], []);
const { table } = useReactDataTable<MyType, MyFilter>({
columns,
data: data?.records,
isLoading: isFetching,
state : { pagination },
manualPagination: true, // pagination is param of the query, so we need to set manualPagination: true
onPaginationChange: setPagination, // we need to tell Tanstack to update our status
});
In case of server side sorting, the following code snippet should be respected.
const { sorting, setSorting} = useReactDataTableState<MyType, MyFilter>(initialSorting: { id: "myKeyOfMyType", desc: false });
const { data, isFetching, refetch } = useMyTypeQuery({
filter: { ... },
options: {
orderBy: sorting?.id,
sortDirection: sorting?.desc ? ListSortDirection.Descending : ListSortDirection.Ascending
},
});
const columns = useMemo(() =>[
...,
columnHelper.accessor(
...
enableSorting: true, // will be managed by Tankstack
...
),
..., []);
const { table } = useReactDataTable<MyType, MyFilter>({
columns,
data: data?.records,
isLoading: isFetching,
state : { pagination },
manualSorting: true, // sorting is param of the query, so we need to set manualSorting: true
onSortingChange: setSorting, // we need to tell Tanstack to update our status
});
In case of server side filtering, the package provides two states for filters:
- columnFilters, the main filter status which changes on every onChange event on the filters.
const { columnFilters , setColumnFilters } = useReactDataTableState<MyType, MyFilter>({ initialColumnFilters : {...}});
const { data, isFetching, refetch } = useMyTypeQuery({
filter: columnFilters,
options: { ... },
});
const columns = useMemo(() =>[
columnHelper.accessor(
...
enableColumnFilter: true,
...
), []);
const { table } = useReactDataTable<MyType, MyFilter>({
columns,
data: data?.records,
isLoading: isFetching,
state : { columnFilters },
manualFiltering: true, // columnFilters is param of the query, so we need to set manualFiltering: true
onColumnFiltersChange: setColumnFilters, // we need to tell Tanstack to update our status
});
- afterSearchFilter, a status that changes only when the Enter button or the search icon is clicked
const { afterSearchFilter, setAfterSearchFilter } = useReactDataTableState<MyType, MyFilter>({ initialColumnFilters : {...}});
const { data, isFetching, refetch } = useMyTypeQuery({
filter: afterSearchFilter, // Notice that passing the columnFilters in the query is a critical error !!!
options: { ... },
});
const columns = useMemo(() =>[
columnHelper.accessor(
...
enableColumnFilter: true,
...
), []);
const { table } = useReactDataTable<MyType, MyFilter>({
columns,
data: data?.records,
isLoading: isFetching,
manualFiltering: true, // afterSearchFilter is param of the query, so we need to set manualFiltering: true
});
// Notice that, passing the onEnter, the table will consider you want to filter only onEnter button
// The columnsFilter state should be ignored
return <ReactDataTable totalRecords={data?.totalRecords} onEnter={setAfterSearchFilter}>
If pagination, sorting and filters have to be managed server side, our package provide a useFullyControlledReactDataTable which set the manual features to true by default.
const { setColumnFilters, setPagination, setSorting, pagination, columnFilters, sorting } = useReactDataTableState<MyType, MyFilter>({ initialColumnFilters : {...}});
const { data, isFetching, refetch } = useMyTypeQuery({
filter: columnFilters,
options: {
limit: pagination.pageSize,
orderBy: sorting?.id,
page: pagination.pageIndex + 1,
sortDirection: sorting?.desc ? ListSortDirection.Descending : ListSortDirection.Ascending
},
});
const columns = useMemo(() =>[
columnHelper.accessor(
...
enableColumnFilter: true,
enableSorting: true,
...
), []);
const { table } = useFullyControlledReactDataTable<T, FavoriteFilter>({
columns,
data: data?.records,
state: {
columnFilters,
pagination,
sorting
},
onColumnFiltersChange: setColumnFilters,
onPaginationChange: setPagination,
onSortingChange: setSorting,
})
This case isn't so trivial and require particular attention when implementing a table.
As described above, if we set manualPagination to true, the table will not execute any actions on pagination. In this case, when filtering, it's expected that the page index is reset to the first one, but this doesn't happen by default.
The datatable package implements this reset logic internally, but this works only for filter rendered by the package itself.
If there is need to render an additional external filter,the page index has to be reset manually when the filter is applied as in the following example.
const { setColumnFilters, setPagination, setSorting, pagination, columnFilters, sorting } = useReactDataTableState<MyType, MyFilter>({ initialColumnFilters : {...}});
const { data, isFetching, refetch } = useMyTypeQuery({
filter: columnFilters,
options: {
limit: pagination.pageSize,
orderBy: sorting?.id,
page: pagination.pageIndex + 1,
sortDirection: sorting?.desc ? ListSortDirection.Descending : ListSortDirection.Ascending
},
});
const columns = useMemo(() =>[
columnHelper.accessor(
...
enableColumnFilter: true,
enableSorting: true,
...
), []);
const { table } = useFullyControlledReactDataTable<T, FavoriteFilter>({
columns,
data: data?.records,
state: {
columnFilters,
pagination,
sorting
},
onColumnFiltersChange: setColumnFilters,
onPaginationChange: setPagination,
onSortingChange: setSorting,
})
return (<>
<DropdownFilter
options={[---]}
title={"My Enum Filter"}
onChange={(e) =>
setColumnFilters((prev) => ({ ...prev, e.target.value }));
table.resetPageIndex(true);
}
/>
<ReactDataTable totalRecords={data?.totalRecords} ...>
</>
)
The table can implement a drag-and-drop rows feature whether the consumer is planning to make rows manually sortable. To implement it, follow these few steps:
- Define a draggable column inside the columns array, via the columnHelper.createDraggableColumn function:
const columns = useMemo(() => [
columnHelper.createDraggableColumn(
"myEntityId", // unique field, ie. the primary key
{}, // column definition, define for instance your own meta
// isEnabled property (optional, true by default), a runtime condition for enabling/disabling the drag-and-drop feature
// draggableElement (optional, hence it displays the custom icon)
),
columnHelper.display(),
columnHelper.accessor(),
...
], [otherDeps]);
- Define in the reactTableOptions of useReactDataTable hook, the getRowId function:
const { table } = useReactDataTable<MyModel, TFilter>({
...
reactTableOptions: {
getRowId: (row) => row.myEntityId, // unique field specified in the point 1, ie. the primary key
},
});
- Define in the ReactDataTable the dragAndDropOptions object:
- enableDragAndDrop for enabling/disabling the feature using a runtime condition.
- onDragEnd(event: DragEndEvent) method for specifying the action which will perform the position update (endpoint call).
<ReactDataTable<T>
...
dragAndDropOptions={{
enableDragAndDrop: true, // your runtime condition here
onDragEnd: (event) => {
// active refers to the current dragged row, so it tracks the current position
// over refers to the last row on which the active was dragged, so it tracks the new position. You need to check its existance
const { active, over } = event;
// Unfortunately the DndContext of @dnd-kit/core exposes just a synchronous method.
// If you need an asynchronous method, you can do as follow
void (async () => {
if(over) {
await updateMyOrder({
activeId: active.id as string,
overId: over.id as string,
});
}
})();
}
}}
/>