This is an article from the series about creating of an advanced Data table component using React, TanStack Table 8, Tailwind CSS and Headless UI.
In the previous article, we provided users an opportunity to pin table columns left or right side of the viewport.
Cell sorting is another way to keep table data organized. This functionality allows users to reorganize data based on a specific column, either in ascending or descending order. This empowers users to explore and analyze data more effectively.
At this exercise, we will implement cell sorting using TanStack table Sorting API. Here's what we'll cover:
Apply sorting to the table data: We'll configure the TanStack table to enable basic sorting functionality.
Create a custom sorting function: We'll explore how to define custom sorting functions for specific data types or sorting needs.
Access and manipulate sorting state: We'll learn how to retrieve and potentially modify the sorting state for advanced use cases.
Here is the demo of the table sorting user experience.
Use TanStack Column Sorting API
TanStack provides us the convenient Sorting API, which we are going to use to implement row ordering logic.
Apply Row model
First thing, we need to add Sorted Row Model from TanStack.
import { FC } from 'react';
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
} from '@tanstack/react-table';
export const DataTable: FC = () => {
const table = useReactTable({
columns,
data: tableData,
getCoreRowModel: getCoreRowModel(),
// apply Sorted Row Model from TanStack
getSortedRowModel: getSortedRowModel(),
});
// ...
Add column actions
Next Step is to extend src/DataTable/features/useColumnActions.tsx
with a two new sorting actions using table sorting API.
/**
* React hook which returns an array
* of table column actions config objects
*/
export const useColumnActions = (
context: HeaderContext<Row, unknown>,
): ColumnAction[] => {
// Get column sorting state using TanStack table API
const isSorted = context.column.getIsSorted();
return useMemo<ColumnAction[]>(
() => [
//...
{
// Use ternary expression to decide which label, text
// or icon to render, according to the sorting state
label: isSorted !== 'asc' ? 'Sort ascending' : 'Clear ascending',
icon:
isSorted !== 'asc' ? (
<Icon name="sort-ascending" className="text-lg" />
) : (
<Icon name="shuffle" className="text-lg" />
),
onClick: () => {
// Conditionally set or unset column sorting state
// using TanStack table API
if (isSorted !== 'asc') {
context.table.setSorting([{ desc: false, id: context.column.id }]);
} else {
context.column.clearSorting();
}
},
},
{
label: isSorted !== 'desc' ? 'Sort descending' : 'Clear descending',
icon:
isSorted !== 'desc' ? (
<Icon name="sort-descending" className="text-lg" />
) : (
<Icon name="shuffle" className="text-lg" />
),
onClick: () => {
if (isSorted !== 'desc') {
context.table.setSorting([{ desc: true, id: context.column.id }]);
} else {
context.column.clearSorting();
}
},
},
],
[context.column, context.table, isSorted],
);
};
Apply highlight
We also have to highlight cells of the column which is currently sorted. Here is the code for this:
<td
key={cell.id}
className={classNames(
// ...
// add cyan highlight when the column is in a sorted state
{
'bg-white': !cell.column.getIsSorted(),
'bg-cyan-100': Boolean(cell.column.getIsSorted()),
}
)}
style={cellStyle}
>
{/*...*/}
</td>
Add custom sorting functions
At the current state, we are capable to properly sort string
, number
and date
cells. But we are facing problems when trying to sort country column.
We made Country
cell to accept the value
prop as a two-letter ISO 3166 region code and render the localized country name in the previous chapter. String sorting, applied to the region codes, doesn't match the one expected from country names.
In order to fix this, we have to provide a custom sorting function for that specific column.
We are going to add our custom sorting function to TanStack table built-in methods such as auto
, alphanumeric
, alphanumericCaseSensitive
, text
, textCaseSensitive
, datetime
, basic
. We will extend it with countryCodesToNames
. Furthermore, we need to create src/DataTable/declarations.d.ts
and register our custom search function here.
import '@tanstack/react-table';
import { Row } from './types.ts';
declare module '@tanstack/react-table' {
interface SortingFns {
countryCodesToNames: SortingFn<unknown>
}
}
In the next step, we will create src/DataTable/features/useSortingFns.ts
hook.
We will get the country name display value from the provided country code in the same way we did with country cell.
const leftName = new Intl.DisplayNames(
LOCALE,
{ type: 'region' })
.of(
left.getValue(id)
);
Then we use Intl.Collator object, which enables language-sensitive string comparison to ensure that country names are set in the correct order.
Here is the full hook code:
import { Row as TableRow, SortingFn } from '@tanstack/react-table';
import { useCallback } from 'react';
import { Row } from './../types.ts';
export const useSortingFns = (locale?: string) => {
const countryCodesToNames: SortingFn<Row[]> = useCallback(
(left: TableRow<Row[]>, right: TableRow<Row[]>, id: string) => {
const leftName = new Intl.DisplayNames(locale, { type: 'region' }).of(
left.getValue(id),
);
const rightName = new Intl.DisplayNames(locale, { type: 'region' }).of(
right.getValue(id),
);
return typeof leftName === 'string' && typeof rightName === 'string'
? new Intl.Collator(locale).compare(leftName, rightName)
: 0;
},
[locale],
);
return { countryCodesToNames };
};
Here is the change we are going to provide to src/DataTable/columnsConfig.tsx
file.
export const columns = [
columnHelper.accessor('address.country', {
sortingFn: 'countryCodesToNames',
//...
})
//...
];
Localization refactoring
As you may notice, we rely on locale
setting in many components. Setting it manually each time is an antipattern, which we are going to refactor now.
We will record our selected locale
into the TanStack table metadata.
First, we will extend src/DataTable/declarations.d.ts
with our locale
meta property definition.
import '@tanstack/react-table';
import { Row } from './types.ts';
declare module '@tanstack/react-table' {
interface TableMeta<TData extends Row> {
locale: string;
}
//...
}
And finally, we apply these changes to src/DataTable/DataTable.tsx
.
//...
import { useSortingFns } from './features/useSortingFns.ts';
type Props = {
//...
locale?: string;
};
export const DataTable: FC<Props> = ({ tableData, locale = 'en-US' }) => {
// create a custom sorting function
const { countryCodesToNames } = useSortingFns(locale);
const table = useReactTable({
meta: {
// record locale to the table meta
locale,
},
sortingFns: {
// set the custom sorting function we created for the table
countryCodesToNames,
},
//...
});
//...
}
We also have to apply the same prop to src/DataTable/cells
components which use locale dependent conversions.
export type Props = {
//...
locale?: string;
};
export const CountryCell: FC<Props> = ({ value, locale }) => {
const formattedValue =
value !== undefined
? new Intl.DisplayNames(locale, { type: 'region' }).of(value)
: '';
//...
};
Now we can easily change locale for all table data.
Demo
Here is a working demo of this exercise.
[Bonus feature] Controllable sorting state
TanStack API allows developers to use controlled sorting design. So every time user sorts column onSortingChange
callback is invoked, and optionally we can disable internal sorting in the favor of the one we provide externally. This might be useful for server side computations.
Create src/DataTable/features/useSorting.ts
hook.
import { useState, useEffect } from "react";
import type { SortingState } from "@tanstack/react-table";
export type Props = {
sortingProp: SortingState;
onSortingChange: (sortingState: SortingState) => void;
};
export const useSorting = ({ sortingProp, onSortingChange }: Props) => {
const [sorting, setSorting] = useState<SortingState>(sortingProp);
useEffect(() => {
setSorting(sortingProp);
}, [sortingProp]);
useEffect(() => {
onSortingChange(sorting);
}, [onSortingChange, sorting]);
return { sorting, setSorting };
};
Then we have to update src/DataTable/DataTable.tsx
import {
useReactTable,
SortingState,
} from '@tanstack/react-table';
type Props = {
//...
/**
* Control table data sorting externally
* @see SortingState
*/
sorting?: SortingState;
/**
* Provide a callback to capture table data sorting changes
* @see SortingState
*/
onSortingChange?: (sortingState: SortingState) => void;
};
export const DataTable: FC<Props> = ({
tableData,
locale = 'en-US',
onSortingChange,
}) => {
//...
const {sorting, setSorting} = useSorting({sortingProp, onSortingChange});
const table = useReactTable({
//..
state: {
sorting,
},
// Set this to true for full external control over sorting state
manualSorting: false,
onSortingChange: setSorting,
})
}
To be continued...
Top comments (0)