Skip to main content

DataTable - Server-Side Operations

Displays a matrix of information with columns, rows, and information that can operate dynamically.

Submit feedback
github
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-query

Second, 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 client
const 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.

  • fetchData accepts an object with pageIndex, pageSize, columnFilters, globalFilter and sorting properties.
  • The function should return an object with rows, pageCount, and rowCount properties.
// Generate mock data; 1000 rows with 4 columns
const 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 filter

Manual 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 NameValue
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)

Table of Contents