Examples › Nested tables with async data loading and sorting

Here is how you can combine asynchronous data loading with sorting in nested tables.

Click on the column headers and the expandable rows in the table below to see it in action:

Company / Department / Employee
Employees / Birth date
No records

The above example is implemented with the following code:

'use client';

import { Box } from '@mantine/core';
import { IconBuilding, IconChevronRight, IconUser, IconUsers } from '@tabler/icons-react';
import { DataTable, DataTableSortStatus } from 'mantine-datatable';
import clsx from 'clsx';
import dayjs from 'dayjs';
import { useState } from 'react';
import {
  useCompaniesAsync,
  useDepartmentsAsync,
  useEmployeesAsync,
  type CompanyWithEmployeeCount,
} from '~/data/nestedAsync';
import classes from './NestedTablesAsyncSortingExample.module.css';

function EmployeesTable({ departmentId, sortStatus }: { departmentId: string; sortStatus: DataTableSortStatus }) {
  const { records, loading } = useEmployeesAsync({ departmentId, sortStatus });

  return (
    <DataTable
      noHeader
      minHeight={100}
      withColumnBorders
      columns={[
        {
          accessor: 'name',
          noWrap: true,
          render: ({ firstName, lastName }) => (
            <Box component="span" ml={40}>
              <IconUser className={classes.icon} />
              <span>
                {firstName} {lastName}
              </span>
            </Box>
          ),
        },
        {
          accessor: 'birthDate',
          render: ({ birthDate }) => dayjs(birthDate).format('DD MMM YYYY'),
          textAlign: 'right',
          width: 200,
        },
      ]}
      records={records}
      fetching={loading && !records.length}
    />
  );
}

function DepartmentsTable({ companyId, sortStatus }: { companyId: string; sortStatus: DataTableSortStatus }) {
  const { records, loading } = useDepartmentsAsync({ companyId, sortStatus });
  const [expandedRecordIds, setExpandedRecordIds] = useState<string[]>([]);

  return (
    <DataTable
      noHeader
      minHeight={100}
      withColumnBorders
      columns={[
        {
          accessor: 'name',
          noWrap: true,
          render: ({ id, name }) => (
            <Box component="span" ml={20}>
              <IconChevronRight
                className={clsx(classes.icon, classes.expandIcon, {
                  [classes.expandIconRotated]: expandedRecordIds.includes(id),
                })}
              />
              <IconUsers className={classes.icon} />
              <span>{name}</span>
            </Box>
          ),
        },
        { accessor: 'employees', textAlign: 'right', width: 200 },
      ]}
      records={records}
      fetching={loading && !records.length}
      rowExpansion={{
        allowMultiple: true,
        expanded: { recordIds: expandedRecordIds, onRecordIdsChange: setExpandedRecordIds },
        content: ({ record }) => <EmployeesTable departmentId={record.id} sortStatus={sortStatus} />,
      }}
    />
  );
}
export function NestedTablesAsyncSortingExample() {
  const [expandedRecordIds, setExpandedRecordIds] = useState<string[]>([]);
  const [sortStatus, setSortStatus] = useState<DataTableSortStatus<CompanyWithEmployeeCount>>({
    columnAccessor: 'name',
    direction: 'asc',
  });
  const { records, loading } = useCompaniesAsync({ sortStatus });

  return (
    <DataTable
      minHeight={160}
      withTableBorder
      highlightOnHover
      sortStatus={sortStatus}
      onSortStatusChange={setSortStatus}
      withColumnBorders
      columns={[
        {
          accessor: 'name',
          sortable: true,
          title: 'Company / Department / Employee',
          noWrap: true,
          render: ({ id, name }) => (
            <>
              <IconChevronRight
                className={clsx(classes.icon, classes.expandIcon, {
                  [classes.expandIconRotated]: expandedRecordIds.includes(id),
                })}
              />
              <IconBuilding className={classes.icon} />
              <span>{name}</span>
            </>
          ),
        },
        {
          accessor: 'details',
          sortable: true,
          title: 'Employees / Birth date',
          render: ({ employees }) => employees,
          textAlign: 'right',
          width: 200,
        },
      ]}
      records={records}
      fetching={loading}
      rowExpansion={{
        allowMultiple: true,
        expanded: { recordIds: expandedRecordIds, onRecordIdsChange: setExpandedRecordIds },
        content: ({ record }) => (
          <DepartmentsTable companyId={record.id} sortStatus={sortStatus as DataTableSortStatus} />
        ),
      }}
    />
  );
}

Head over to the next example to discover more features.