MRT logoMaterial React Table

Lazy Sub-Rows Example

If you have a ton of nested data that you want to display, but you don't want to fetch it all up front, you can set up Material React Table to only fetch the sub-rows data when the user expands the row.

There are quite a few ways in which you could implement fetching sub-rows lazily. This example is just one way to do it.

This example combines concepts from the React Query Example and the Expanding Parsed Tree Example.

CRUD Examples
More Examples

Demo

Open StackblitzOpen Code SandboxOpen on GitHub
0-0 of 0

Source Code

1import { lazy, Suspense, useMemo, useState } from 'react';
2import {
3 MaterialReactTable,
4 useMaterialReactTable,
5 type MRT_ColumnDef,
6 type MRT_PaginationState,
7 type MRT_SortingState,
8 type MRT_ExpandedState,
9} from 'material-react-table';
10import {
11 QueryClient,
12 QueryClientProvider,
13 keepPreviousData,
14 useQuery,
15} from '@tanstack/react-query'; //note: this is TanStack React Query V5
16
17//Your API response shape will probably be different. Knowing a total row count is important though.
18type UserApiResponse = {
19 data: Array<User>;
20 meta: {
21 totalRowCount: number;
22 };
23};
24
25type User = {
26 id: string;
27 firstName: string;
28 lastName: string;
29 email: string;
30 state: string;
31 managerId: string | null; //row's parent row id
32 subordinateIds: string[]; //or some type of boolean that indicates that there are sub-rows
33};
34
35const columns: MRT_ColumnDef<User>[] = [
36 //column definitions...
54];
55
56const Example = () => {
57 const [sorting, setSorting] = useState<MRT_SortingState>([]);
58 const [pagination, setPagination] = useState<MRT_PaginationState>({
59 pageIndex: 0,
60 pageSize: 10,
61 });
62 const [expanded, setExpanded] = useState<MRT_ExpandedState>({}); //Record<string, boolean> | true
63
64 //which rows have sub-rows expanded and need their direct sub-rows to be included in the API call
65 const expandedRowIds: string[] | 'all' = useMemo(
66 () =>
67 expanded === true
68 ? 'all'
69 : Object.entries(expanded)
70 .filter(([_managerId, isExpanded]) => isExpanded)
71 .map(([managerId]) => managerId),
72 [expanded],
73 );
74
75 const {
76 data: { data = [], meta } = {},
77 isError,
78 isRefetching,
79 isLoading,
80 } = useFetchUsers({
81 pagination,
82 sorting,
83 expandedRowIds,
84 });
85
86 //get data for root rows only (top of the tree data)
87 const rootData = useMemo(() => data.filter((r) => !r.managerId), [data]);
88
89 const table = useMaterialReactTable({
90 columns,
91 data: rootData,
92 enableExpanding: true, //enable expanding column
93 enableFilters: false,
94 //tell MRT which rows have additional sub-rows that can be fetched
95 getRowCanExpand: (row) => !!row.original.subordinateIds.length, //just some type of boolean
96 //identify rows by the user's id
97 getRowId: (row) => row.id,
98 //if data is delivered in a flat array, MRT can convert it to a tree structure
99 //though it's usually better if the API can construct the nested structure before this point
100 getSubRows: (row) => data.filter((r) => r.managerId === row.id), //parse flat array into tree structure
101 // paginateExpandedRows: false, //the back-end in this example is acting as if this option is false
102 manualPagination: true, //turn off built-in client-side pagination
103 manualSorting: true, //turn off built-in client-side sorting
104 muiToolbarAlertBannerProps: isError
105 ? {
106 color: 'error',
107 children: 'Error loading data',
108 }
109 : undefined,
110 onExpandedChange: setExpanded,
111 onPaginationChange: setPagination,
112 onSortingChange: setSorting,
113 rowCount: meta?.totalRowCount ?? 0,
114 state: {
115 expanded,
116 isLoading,
117 pagination,
118 showAlertBanner: isError,
119 showProgressBars: isRefetching,
120 sorting,
121 },
122 });
123
124 return <MaterialReactTable table={table} />;
125};
126
127//react query setup in App.tsx
128const ReactQueryDevtoolsProduction = lazy(() =>
129 import('@tanstack/react-query-devtools/build/modern/production.js').then(
130 (d) => ({
131 default: d.ReactQueryDevtools,
132 }),
133 ),
134);
135
136const queryClient = new QueryClient();
137
138export default function App() {
139 return (
140 <QueryClientProvider client={queryClient}>
141 <Example />
142 <Suspense fallback={null}>
143 <ReactQueryDevtoolsProduction />
144 </Suspense>
145 </QueryClientProvider>
146 );
147}
148
149//fetch user hook
150const useFetchUsers = ({
151 pagination,
152 sorting,
153 expandedRowIds,
154}: {
155 pagination: MRT_PaginationState;
156 sorting: MRT_SortingState;
157 expandedRowIds: string[] | 'all';
158}) => {
159 return useQuery<UserApiResponse>({
160 queryKey: [
161 'users', //give a unique key for this query
162 {
163 pagination, //refetch when pagination changes
164 sorting, //refetch when sorting changes
165 expandedRowIds,
166 },
167 ],
168 queryFn: async () => {
169 const fetchURL = new URL('/api/treedata', location.origin); // nextjs api route
170
171 //read our state and pass it to the API as query params
172 fetchURL.searchParams.set(
173 'start',
174 `${pagination.pageIndex * pagination.pageSize}`,
175 );
176 fetchURL.searchParams.set('size', `${pagination.pageSize}`);
177 fetchURL.searchParams.set('sorting', JSON.stringify(sorting ?? []));
178 fetchURL.searchParams.set(
179 'expandedRowIds',
180 expandedRowIds === 'all' ? 'all' : JSON.stringify(expandedRowIds ?? []),
181 );
182
183 //use whatever fetch library you want, fetch, axios, etc
184 const response = await fetch(fetchURL.href);
185 const json = (await response.json()) as UserApiResponse;
186 return json;
187 },
188 placeholderData: keepPreviousData, //don't go to 0 rows when refetching or paginating to next page
189 });
190};
191

View Extra Storybook Examples