Theme

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.
Click on the expandable rows in the table below to see it in action:
Sipes Inc
58
Runolfsdottir - Cummerata
45
Johnston LLC
60
Crist and Sons
68
Schmidt and Sons
28
Nicolas Group
34
Kub and Sons
33
Jakubowski - Rolfson
74
Welch - Tremblay
50
Mueller, Hodkiewicz and Beahan
50
No records
export default function NestedTablesExampleAsync() {
const [expandedRecordIds, setExpandedRecordIds] = useState<string[]>([]);
const { cx, classes } = useStyles();
return (
<DataTable
withBorder
withColumnBorders
highlightOnHover
columns={[
{
accessor: 'name',
title: 'Company › Department › Employee',
render: ({ id, name }) => (
<Group spacing="xs">
<IconChevronRight
size="0.9em"
className={cx(classes.expandIcon, {
[classes.expandIconRotated]: expandedRecordIds.includes(id),
})}
/>
<IconBuilding size="0.9em" />
<Text>{name}</Text>
</Group>
),
},
{ accessor: 'employees', title: 'Employees › Birth date', textAlignment: 'right', width: 200 },
]}
records={companies}
rowExpansion={{
allowMultiple: true,
expanded: { recordIds: expandedRecordIds, onRecordIdsChange: setExpandedRecordIds },
content: ({ record }) => <DepartmentsTable companyId={record.id} />,
}}
/>
);
}
function DepartmentsTable({ companyId }: { companyId: string }) {
const { records, loading } = useDepartmentsAsync({ companyId });
const [expandedRecordIds, setExpandedRecordIds] = useState<string[]>([]);
const { cx, classes } = useStyles();
return (
<DataTable
noHeader
minHeight={100}
columns={[
{
accessor: 'name',
render: ({ id, name }) => (
<Group ml="lg" spacing="xs" noWrap>
<IconChevronRight
size="0.9em"
className={cx(classes.expandIcon, {
[classes.expandIconRotated]: expandedRecordIds.includes(id),
})}
/>
<IconUsers size="0.9em" />
<Text>{name}</Text>
</Group>
),
},
{ accessor: 'employees', textAlignment: 'right', width: 200 },
]}
records={records}
fetching={loading}
rowExpansion={{
allowMultiple: true,
expanded: { recordIds: expandedRecordIds, onRecordIdsChange: setExpandedRecordIds },
content: ({ record }) => <EmployeesTable departmentId={record.id} />,
}}
/>
);
}
function EmployeesTable({ departmentId }: { departmentId: string }) {
const { records, loading } = useEmployeesAsync({ departmentId });
const { classes } = useStyles();
return (
<DataTable
noHeader
minHeight={100}
columns={[
{
accessor: 'name',
render: ({ firstName, lastName }) => (
<Group spacing="xs" noWrap className={classes.employeeName}>
<IconUser size="0.9em" />
<Text>
{firstName} {lastName}
</Text>
</Group>
),
},
{
accessor: 'birthDate',
render: ({ birthDate }) => dayjs(birthDate).format('DD MMM YYYY'),
textAlignment: 'right',
width: 200,
},
]}
records={records}
fetching={loading}
/>
);
}
const useStyles = createStyles((theme) => ({
expandIcon: {
transition: 'transform 0.2s ease',
},
expandIconRotated: {
transform: 'rotate(90deg)',
},
employeeName: {
marginLeft: px(theme.spacing.xl) * 2,
},
}));
import { sortBy } from 'lodash';
import { useEffect, useState } from 'react';
import { DataTableSortStatus } from '~/../package';
import delay from '~/lib/delay';
import useIsMounted from '~/lib/useIsMounted';
import { companies as companyData, departments as departmentData, employees } from './index';
// Departments with employees count
export const departments = departmentData.map((department) => ({
...department,
employees: employees.filter((employee) => employee.department.id === department.id)?.length || 0,
}));
// Companies with employees count
export const companies = companyData.map((company) => ({
...company,
employees: departments
.filter((department) => department.company.id === company.id)
.reduce((sum, department) => sum + department.employees, 0),
}));
// Employees
export { employees };
// Hook simulating async companies fetching
export function useCompaniesAsync({ sortStatus }: { sortStatus: DataTableSortStatus }) {
const isMounted = useIsMounted();
const [records, setRecords] = useState<typeof companies>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (isMounted()) {
(async () => {
setLoading(true);
await delay({ min: 500, max: 800 });
if (isMounted()) {
const newRecords = sortBy(
companies,
sortStatus.columnAccessor === 'details' ? 'employees' : sortStatus.columnAccessor
);
if (sortStatus.direction === 'desc') newRecords.reverse();
setRecords(newRecords);
setLoading(false);
}
})();
}
}, [isMounted, sortStatus]);
return { records, loading };
}
// Hook simulating async departments fetching by company id
export function useDepartmentsAsync({
companyId,
sortStatus,
}: {
companyId: string;
sortStatus?: DataTableSortStatus;
}) {
const isMounted = useIsMounted();
const [records, setRecords] = useState<typeof departments>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (isMounted()) {
(async () => {
setLoading(true);
await delay({ min: 500, max: 800 });
if (isMounted()) {
let newRecords = departments.filter((department) => department.company.id === companyId);
if (sortStatus) {
newRecords = sortBy(
newRecords,
sortStatus.columnAccessor === 'details' ? 'employees' : sortStatus.columnAccessor
);
if (sortStatus.direction === 'desc') newRecords.reverse();
}
setRecords(newRecords);
setLoading(false);
}
})();
}
}, [companyId, isMounted, sortStatus]);
return { records, loading };
}
// Hook simulating async employees fetching by department id
export function useEmployeesAsync({
departmentId,
sortStatus,
}: {
departmentId: string;
sortStatus?: DataTableSortStatus;
}) {
const isMounted = useIsMounted();
const [records, setRecords] = useState<typeof employees>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (isMounted()) {
(async () => {
setLoading(true);
await delay({ min: 500, max: 800 });
if (isMounted()) {
let newRecords = employees.filter((employee) => employee.department.id === departmentId);
if (sortStatus) {
newRecords = sortBy(
newRecords,
sortStatus.columnAccessor === 'name'
? ({ firstName, lastName }) => `${firstName} ${lastName}`
: 'birthDate'
);
if (sortStatus.direction === 'desc') newRecords.reverse();
}
setRecords(newRecords);
setLoading(false);
}
})();
}
}, [departmentId, isMounted, sortStatus]);
return { records, loading };
}
Head over to the next example to see how you could combine this behavior with sorting.

Mantine DataTable is trusted by

MIT LicenseSponsor the author
Built by Ionut-Cristian Florescu and these awesome people.
Please sponsor the project if you find it useful.
GitHub StarsNPM Downloads