Theme

Examples › Nested tables with async data loading and sorting

Click on the column headers and the expandable rows in the table below to see it in action:
No records
export default function NestedTablesExampleAsyncWithSorting() {
const [expandedRecordIds, setExpandedRecordIds] = useState<string[]>([]);
const [sortStatus, setSortStatus] = useState<DataTableSortStatus>({ columnAccessor: 'name', direction: 'asc' });
const { records, loading } = useCompaniesAsync({ sortStatus });
const { cx, classes } = useStyles();
return (
<DataTable
minHeight={160}
withBorder
withColumnBorders
highlightOnHover
sortStatus={sortStatus}
onSortStatusChange={setSortStatus}
columns={[
{
accessor: 'name',
sortable: true,
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: 'details',
sortable: true,
title: 'Employees › Birth date',
render: ({ employees }) => employees,
textAlignment: 'right',
width: 200,
},
]}
records={records}
fetching={loading}
rowExpansion={{
allowMultiple: true,
expanded: { recordIds: expandedRecordIds, onRecordIdsChange: setExpandedRecordIds },
content: ({ record }) => <DepartmentsTable companyId={record.id} sortStatus={sortStatus} />,
}}
/>
);
}
function DepartmentsTable({ companyId, sortStatus }: { companyId: string; sortStatus: DataTableSortStatus }) {
const { records, loading } = useDepartmentsAsync({ companyId, sortStatus });
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 && !records.length}
rowExpansion={{
allowMultiple: true,
expanded: { recordIds: expandedRecordIds, onRecordIdsChange: setExpandedRecordIds },
content: ({ record }) => <EmployeesTable departmentId={record.id} sortStatus={sortStatus} />,
}}
/>
);
}
function EmployeesTable({ departmentId, sortStatus }: { departmentId: string; sortStatus: DataTableSortStatus }) {
const { records, loading } = useEmployeesAsync({ departmentId, sortStatus });
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 && !records.length}
/>
);
}
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 };
}

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