Examples › Nested tables with async data loading
Since the row expansion content
function is lazily executed when a row is expanded to prevent creating unnecessary DOM elements, you can use this behavior to asynchronously load data for nested tables.
Warning
Nested tables do not work with column pinning -
pinFirstColumn
and pinLastColumn
.Click on the expandable rows in the table below to see it in action:
Company / Department / Employee | Employees / Birth date |
---|---|
Feest, Bogan and Herzog | 37 |
Cummerata - Kuhlman | 67 |
Goyette Inc | 63 |
Runte Inc | 87 |
Goldner, Rohan and Lehner | 29 |
Doyle, Hodkiewicz and O'Connell | 61 |
Rau - O'Hara | 41 |
Tillman - Jacobi | 46 |
Connelly, Feest and Hodkiewicz | 38 |
Shanahan, Robel and Beier | 31 |
Kling - McLaughlin | 0 |
Jogi - McLaughlin | 0 |
Jogi - McLaughlin | 0 |
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 } from 'mantine-datatable';
import clsx from 'clsx';
import dayjs from 'dayjs';
import { useState } from 'react';
import { companies } from '~/data/nested';
import { useDepartmentsAsync, useEmployeesAsync } from '~/data/nestedAsync';
import classes from './NestedTablesAsyncExample.module.css';
function EmployeesTable({ departmentId }: { departmentId: string }) {
const { records, loading } = useEmployeesAsync({ departmentId });
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}
/>
);
}
function DepartmentsTable({ companyId }: { companyId: string }) {
const { records, loading } = useDepartmentsAsync({ companyId });
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}
rowExpansion={{
allowMultiple: true,
expanded: { recordIds: expandedRecordIds, onRecordIdsChange: setExpandedRecordIds },
content: ({ record }) => <EmployeesTable departmentId={record.id} />,
}}
/>
);
}
export function NestedTablesAsyncExample() {
const [expandedRecordIds, setExpandedRecordIds] = useState<string[]>([]);
return (
<DataTable
withTableBorder
withColumnBorders
highlightOnHover
columns={[
{
accessor: 'name',
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: 'employees', title: 'Employees / Birth date', textAlign: 'right', width: 200 },
]}
records={companies}
rowExpansion={{
allowMultiple: true,
expanded: { recordIds: expandedRecordIds, onRecordIdsChange: setExpandedRecordIds },
content: ({ record }) => <DepartmentsTable companyId={record.id} />,
}}
/>
);
}
Head over to the next example to see how you could combine this behavior with sorting.