import { DataTable } from '@uhg-abyss/web/ui/DataTable';import { useDataTable } from '@uhg-abyss/web/hooks/useDataTable';If you have a large data set, you may not want to load all of that data into the client's browser at once, as this can cause performance issues. In this case, the best approach is to handle sorting, filtering, and pagination on a backend server. This means that the server will only send the data that is needed for the current page and the client will not have to load all of the data at once.
However, a lot of developers underestimate just how many rows can be loaded locally without a performance hit. DataTable is able to handle a significant amount of data—on the order of thousands of rows—with decent performance for client-side sorting, filtering, and pagination. This doesn't necessarily mean that your application will be able to handle that many rows, but if your table is only going to have a few thousand rows at most, you might be able to take advantage of the client-side features, which are much easier to implement.
Disclaimer: To use a back-end server, teams must handle all filtering, sorting, and pagination logic manually. This setup requires more effort and careful management to ensure optimal performance and correct behavior. We recommend reading through the documentation for sorting, filtering, and pagination to understand how these features work before implementing them with a remote data source.
Setup
To implement server-side operations, we recommend using TanStack Query. You are welcome to use any other data-fetching library you choose, but we will be using TanStack Query in these examples.
First, install the @tanstack/react-query package.
npm i @tanstack/react-querySecond, add a QueryClientProvider to the root of your application and provide it with a QueryClient.
Note: You should only have one QueryClientProvider and QueryClient in your application.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
// Create a clientconst queryClient = new QueryClient();
export const browser = () => { return ( // Provide the client to your App <QueryClientProvider client={queryClient}> <AbyssProvider theme={theme} router={router} /> </QueryClientProvider> );};Full example
This example demonstrates using sorting, filtering, and pagination with a remote data source, using TanStack Query to fetch the data. Subsequent sections will explain in more detail how to implement sorting, filtering, and pagination.
First, we need to create a function that fetches the data from the server. This function should accept parameters for pagination, filtering, and sorting, and return the data in the format expected by the DataTable.
fetchDataaccepts an object withpageIndex,pageSize,columnFilters,globalFilterandsortingproperties.- The function should return an object with
rows,pageCount, androwCountproperties.
// Generate mock data; 1000 rows with 4 columnsconst data = createData(1000, ['index', 'date', 'number', 'status']);
export const fetchData = async ({ pageIndex, pageSize, columnFilters, globalFilter, sorting,}) => { // Simulate some network latency await new Promise((r) => { return setTimeout(r, 1000); });
let filteredData = [...data];
// Apply global filter if (globalFilter) { filteredData = filteredData.filter((row) => { return Object.values(row).some((value) => { return value .toString() .toLowerCase() .includes(globalFilter.toLowerCase()); }); }); }
// Apply column filters columnFilters.forEach((filter) => { const { id, value } = filter; filteredData = filteredData.filter((row) => { if (!value.filters || value.filters.length === 0) { return true; }
const matchType = value.matchType || 'all'; const matchAny = matchType === 'any';
// Apply each filter condition const results = value.filters.map((condition) => { if (condition.condition === 'equals') { return row[id] === condition.value; } if (condition.condition === 'contains') { return row[id] .toString() .toLowerCase() .includes(condition.value.toLowerCase()); } // Add more filter conditions as needed (e.g., "startsWith", "greaterThan", etc.) return false; });
// Return based on match type ("any" uses OR logic, "all" uses AND logic) return matchAny ? results.some(Boolean) : results.every(Boolean); }); });
// Apply sorting sorting.forEach((sort) => { const { id, desc } = sort; filteredData.sort((a, b) => { if (a[id] < b[id]) return desc ? 1 : -1; if (a[id] > b[id]) return desc ? -1 : 1; return 0; }); });
// Paginate the data const paginatedData = filteredData.slice( pageIndex * pageSize, (pageIndex + 1) * pageSize );
return { rows: paginatedData, pageCount: Math.ceil(filteredData.length / pageSize), rowCount: filteredData.length, };};Second, we need to use TanStack Query's useQuery hook with our fetchData function.
import { keepPreviousData, useQuery } from '@tanstack/react-query';import { fetchData } from '/path/to/fetchData';// ...
const DataTableApiPagination = () => { // State for pagination, filtering, and sorting const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10, }); const [columnFilters, setColumnFilters] = useState([]); const [globalFilter, setGlobalFilter] = useState(''); const [sorting, setSorting] = useState([]);
// Retrieve data from the server const dataQuery = useQuery({ queryKey: ['data', pagination, columnFilters, globalFilter, sorting], queryFn: () => { return fetchData({ pageIndex: pagination.pageIndex, pageSize: pagination.pageSize, columnFilters, globalFilter, sorting, }); }, placeholderData: keepPreviousData, }); const defaultData = React.useMemo(() => [], []);
const dataTableProps = useDataTable({ initialData: dataQuery.data?.rows ?? defaultData, initialColumns: columns, tableConfig: { rowCount: dataQuery.data?.rowCount, // ... state: { pagination, columnFilters, globalFilter, sorting, }, // ... onColumnFiltersChange: setColumnFilters, onPaginationChange: setPagination, onGlobalFilterChange: setGlobalFilter, onSortingChange: setSorting, // ... manualFiltering: true; manualSorting: true; manualPagination: true; }, });
// If you aren't using the `isLoading` prop, the table will display the previous data until the new data is fetched. return ( <DataTable title="Server-Side-Pagination" tableState={dataTableProps}> <DataTable.Table isLoading={dataQuery.isFetching} /> <DataTable.SlotWrapper css={{ overflowX: 'auto' }}> <DataTable.Pagination /> </DataTable.SlotWrapper> </DataTable> );};And now, putting it all together:
Manual pagination
Manual pagination is configured very similarly to client-side pagination. The only difference is the use of tableConfig.manualPagination instead of tableConfig.enablePagination.
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10,});
const dataTableProps = useDataTable({ // ... tableConfig: { state: { pagination, }, onPaginationChange: setPagination, manualPagination: true; }, // ...});pagination should be an object with the following structure:
{ pageIndex: 0, // The current page index pageSize: 10, // The number of rows per page}Teams are responsible for managing the state and updating the pageIndex and pageSize values. Without updating the pageIndex value, the table could display an empty page if the current pageIndex exceeds the number of pages available. Thus, we recommend resetting the pageIndex to 0 whenever any filter values change, as shown below.
useEffect(() => { setPagination((prev) => { return { ...prev, pageIndex: 0 }; });}, [columnFilters, globalFilter, sorting]);Manual global filtering
Manual global filtering is configured very similarly to client-side global filtering. The only difference is the use of tableConfig.manualFiltering.
const [globalFilter, setGlobalFilter] = useState('');
const dataTableProps = useDataTable({ // ... tableConfig: { state: { globalFilter, }, onGlobalFilterChange: setGlobalFilter, manualFiltering: true; // ... },});globalFilter should be a string that represents the value of the global filter.
'17'; // The value of the global filterManual column filtering
Manual column filtering is configured very similarly to client-side column filtering. The only difference is the use of tableConfig.manualFiltering instead of tableConfig.enableColumnFilters.
const [columnFilters, setColumnFilters] = useState([]);
const dataTableProps = useDataTable({ // ... tableConfig: { state: { columnFilters, }, onColumnFiltersChange: setColumnFilters, manualFiltering: true; // ... },});columnFilters should be an array of objects, where each object represents a filter for a specific column.
Each filter object should have the following structure:
[ { id: 'col2', // The ID of the column to which the filter applies value: { matchType: 'all', // The type of match (e.g., 'all', 'any') filters: [ { condition: 'equals', // The condition to apply (e.g., 'equals', 'contains') value: '4', // The value to filter by }, ], }, }, { // ... },];Manual sorting
Manual sorting is configured very similarly to client-side sorting. The only difference is the use of tableConfig.manualSorting instead of tableConfig.enableSorting.
const [sorting, setSorting] = useState([]);
const dataTableProps = useDataTable({ // ... tableConfig: { state: { sorting, }, onSortingChange: setSorting, manualSorting: true; }, // ...});sorting should be an array of objects, where each object represents a sort for a specific column.
{ "id": "col1", // The ID of the column to which the sorting applies "desc": false // true for descending, false for ascending}Loading state
When using a remote data source, we recommend adding a loading state to the table to prevent it from displaying stale data while new data is being fetched and to improve the user experience. Use the isLoading prop on DataTable.Table to place the table in a loading state.
Component Tokens
Note: Click on the token row to copy the token to your clipboard.
DataTable Tokens
| Token Name | Value | |
|---|---|---|
| data-table.color.border.column-header.drag | #002677 | |
| data-table.color.border.root | #CBCCCD | |
| data-table.color.border.row.drag | #002677 | |
| data-table.color.border.table | #CBCCCD | |
| data-table.color.icon.column-header-menus.grouping.active | #002677 | |
| data-table.color.icon.column-header-menus.grouping.hover | #004BA0 | |
| data-table.color.icon.column-header-menus.grouping.rest | #196ECF | |
| data-table.color.icon.column-header-menus.sorting.active | #002677 | |
| data-table.color.icon.column-header-menus.sorting.hover | #004BA0 | |
| data-table.color.icon.column-header-menus.sorting.rest | #196ECF | |
| data-table.color.icon.drag-handle.active | #002677 | |
| data-table.color.icon.drag-handle.hover | #004BA0 | |
| data-table.color.icon.drag-handle.rest | #196ECF | |
| data-table.color.icon.expander.active | #002677 | |
| data-table.color.icon.expander.disabled | #7D7F81 | |
| data-table.color.icon.expander.hover | #004BA0 | |
| data-table.color.icon.expander.rest | #196ECF | |
| data-table.color.icon.utility.drag-alternative.active | #000000 | |
| data-table.color.icon.utility.drag-alternative.disabled | #7D7F81 | |
| data-table.color.icon.utility.drag-alternative.hover | #323334 | |
| data-table.color.icon.utility.drag-alternative.rest | #4B4D4F | |
| data-table.color.icon.utility.filter.active | #002677 | |
| data-table.color.icon.utility.filter.hover | #004BA0 | |
| data-table.color.icon.utility.filter.rest | #196ECF | |
| data-table.color.surface.column-header.active | #E5F8FB | |
| data-table.color.surface.column-header.default | #F3F3F3 | |
| data-table.color.surface.column-header.drag | #E5F8FB | |
| data-table.color.surface.footer | #F3F3F3 | |
| data-table.color.surface.header | #FFFFFF | |
| data-table.color.surface.root | #FFFFFF | |
| data-table.color.surface.row.drag | #E5F8FB | |
| data-table.color.surface.row.even | #FAFCFF | |
| data-table.color.surface.row.highlighted | #E5F8FB | |
| data-table.color.surface.row.hover | #F3F3F3 | |
| data-table.color.surface.row.odd | #FFFFFF | |
| data-table.color.surface.table | #FFFFFF | |
| data-table.color.text.cell | #4B4D4F | |
| data-table.color.text.column-header | #4B4D4F | |
| data-table.color.text.header.heading | #002677 | |
| data-table.color.text.header.paragraph | #4B4D4F | |
| data-table.border-radius.all.container | 8px | |
| data-table.border-width.all.column-header.drag | 2px | |
| data-table.border-width.all.root | 1px | |
| data-table.border-width.all.row.drag | 2px | |
| data-table.border-width.all.table | 1px | |
| data-table.sizing.all.icon.column-header-menus | 20px | |
| data-table.sizing.all.icon.drag-handle-row | 24px | |
| data-table.sizing.all.icon.expander-column | 24px | |
| data-table.sizing.all.icon.utility.drag-alternative | 20px | |
| data-table.sizing.all.icon.utility.filter | 20px | |
| data-table.sizing.height.cell.comfortable | 48px | |
| data-table.sizing.height.cell.compact | 32px | |
| data-table.sizing.height.cell.cozy | 40px | |
| data-table.spacing.gap.horizontal.button-group | 8px | |
| data-table.spacing.gap.horizontal.cell | 4px | |
| data-table.spacing.gap.horizontal.drag-alternative | 8px | |
| data-table.spacing.gap.horizontal.input-container | 8px | |
| data-table.spacing.gap.horizontal.slot-wrapper | 24px | |
| data-table.spacing.gap.vertical.column-header | 2px | |
| data-table.spacing.gap.vertical.header | 4px | |
| data-table.spacing.gap.filter-two-inputs | 16px | |
| data-table.spacing.padding.all.column-header | 8px | |
| data-table.spacing.padding.all.column-header-menus | 2px | |
| data-table.spacing.padding.all.header | 16px | |
| data-table.spacing.padding.all.result-text | 16px | |
| data-table.spacing.padding.all.slot-wrapper | 16px | |
| data-table.spacing.padding.horizontal.cell | 8px | |
| data-table.spacing.padding.vertical.button-group | 8px | |
| data-table.spacing.padding.vertical.cell | 4px | |
| data-table.elevation.column.pinned.left | 6px 0px 8px -2px rgba(0,0,0,0.16) | |
| data-table.elevation.column.pinned.right | -6px 0px 8px -2px rgba(0,0,0,0.16) | |
| data-table.elevation.column-header | 0px 6px 8px -2px rgba(0,0,0,0.16) | |
| data-table.elevation.table-settings-dropdown.section-header | 0px 2px 4px -2px rgba(0,0,0,0.16) |