DEV Community

Cover image for Let's create Data Table. Part 5: Cell sorting and localization
Dima Vyshniakov
Dima Vyshniakov

Posted on

Let's create Data Table. Part 5: Cell sorting and localization

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.

Demo of column sorting

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(),
  });

  // ...

Enter fullscreen mode Exit fullscreen mode

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],
  );
};

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.

Country sorting bug

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>
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
);
Enter fullscreen mode Exit fullscreen mode

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 };
};
Enter fullscreen mode Exit fullscreen mode

Here is the change we are going to provide to src/DataTable/columnsConfig.tsx file.

export const columns = [
  columnHelper.accessor('address.country', {
    sortingFn: 'countryCodesToNames',
    //...
  })
  //...
];
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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,
    },
    //...
  });
  //...
}

Enter fullscreen mode Exit fullscreen mode

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)
      : '';

  //...
};
Enter fullscreen mode Exit fullscreen mode

Now we can easily change locale for all table data.

Localized data demo

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 };
};
Enter fullscreen mode Exit fullscreen mode

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,
  })
}

Enter fullscreen mode Exit fullscreen mode

To be continued...

Top comments (0)