MRT logoMaterial React Table

Lazy Detail Panel Example

Fetching the additional data for the detail panels only after the user clicks to expand the row can be a good way to improve performance, and it is pretty easy to implement in Material React Table.

It's even easier if you are using React Query. In this example, react query is used to fetch the data for the detail panel, but only after the user clicks to expand the row. The data is also cached for a certain period of time so that when the detail panel is closed and reopened, the data is already available and won't need to be re-fetched.

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_ColumnFiltersState,
7 type MRT_PaginationState,
8 type MRT_SortingState,
9 type MRT_Row,
10} from 'material-react-table';
11import { Alert, CircularProgress, Stack } from '@mui/material';
12import AddIcon from '@mui/icons-material/Add';
13import MinusIcon from '@mui/icons-material/Remove';
14import {
15 QueryClient,
16 QueryClientProvider,
17 keepPreviousData,
18 useQuery,
19} from '@tanstack/react-query'; //note: this is TanStack React Query V5
20
21//Your API response shape will probably be different. Knowing a total row count is important though.
22type UserApiResponse = {
23 data: Array<User>;
24 meta: {
25 totalRowCount: number;
26 };
27};
28
29type User = {
30 firstName: string;
31 lastName: string;
32 address: string;
33 state: string;
34 phoneNumber: string;
35 lastLogin: Date;
36};
37
38type FullUserInfoApiResponse = FullUserInfo;
39
40type FullUserInfo = User & {
41 favoriteMusic: string;
42 favoriteSong: string;
43 quote: string;
44};
45
46const DetailPanel = ({ row }: { row: MRT_Row<User> }) => {
47 const {
48 data: userInfo,
49 isLoading,
50 isError,
51 } = useFetchUserInfo(
52 {
53 phoneNumber: row.id, //the row id is set to the user's phone number
54 },
55 {
56 enabled: row.getIsExpanded(),
57 },
58 );
59 if (isLoading) return <CircularProgress />;
60 if (isError) return <Alert severity="error">Error Loading User Info</Alert>;
61
62 const { favoriteMusic, favoriteSong, quote } = userInfo ?? {};
63
64 return (
65 <Stack gap="0.5rem" minHeight="00px">
66 <div>
67 <b>Favorite Music:</b> {favoriteMusic}
68 </div>
69 <div>
70 <b>Favorite Song:</b> {favoriteSong}
71 </div>
72 <div>
73 <b>Quote:</b> {quote}
74 </div>
75 </Stack>
76 );
77};
78
79const Example = () => {
80 //manage our own state for stuff we want to pass to the API
81 const [columnFilters, setColumnFilters] = useState<MRT_ColumnFiltersState>(
82 [],
83 );
84 const [globalFilter, setGlobalFilter] = useState('');
85 const [sorting, setSorting] = useState<MRT_SortingState>([]);
86 const [pagination, setPagination] = useState<MRT_PaginationState>({
87 pageIndex: 0,
88 pageSize: 5,
89 });
90
91 const {
92 data: { data = [], meta } = {},
93 isError,
94 isRefetching,
95 isLoading,
96 } = useFetchUsers({
97 columnFilters,
98 globalFilter,
99 pagination,
100 sorting,
101 });
102
103 const columns = useMemo<MRT_ColumnDef<User>[]>(
104 //column definitions...
129 );
130
131 const table = useMaterialReactTable({
132 columns,
133 data,
134 getRowId: (row) => row.phoneNumber,
135 manualFiltering: true, //turn off built-in client-side filtering
136 manualPagination: true, //turn off built-in client-side pagination
137 manualSorting: true, //turn off built-in client-side sorting
138 muiExpandButtonProps: ({ row }) => ({
139 children: row.getIsExpanded() ? <MinusIcon /> : <AddIcon />,
140 }),
141 muiToolbarAlertBannerProps: isError
142 ? {
143 color: 'error',
144 children: 'Error loading data',
145 }
146 : undefined,
147 onColumnFiltersChange: setColumnFilters,
148 onGlobalFilterChange: setGlobalFilter,
149 onPaginationChange: setPagination,
150 onSortingChange: setSorting,
151 renderDetailPanel: ({ row }) => <DetailPanel row={row} />,
152 rowCount: meta?.totalRowCount ?? 0,
153 state: {
154 columnFilters,
155 globalFilter,
156 isLoading,
157 pagination,
158 showAlertBanner: isError,
159 showProgressBars: isRefetching,
160 sorting,
161 },
162 });
163
164 return <MaterialReactTable table={table} />;
165};
166
167//react query setup in App.tsx
168const ReactQueryDevtoolsProduction = lazy(() =>
169 import('@tanstack/react-query-devtools/build/modern/production.js').then(
170 (d) => ({
171 default: d.ReactQueryDevtools,
172 }),
173 ),
174);
175
176const queryClient = new QueryClient();
177
178export default function App() {
179 return (
180 <QueryClientProvider client={queryClient}>
181 <Example />
182 <Suspense fallback={null}>
183 <ReactQueryDevtoolsProduction />
184 </Suspense>
185 </QueryClientProvider>
186 );
187}
188
189//fetch user hook
190const useFetchUsers = ({
191 columnFilters,
192 globalFilter,
193 pagination,
194 sorting,
195}: {
196 columnFilters: MRT_ColumnFiltersState;
197 globalFilter: string;
198 pagination: MRT_PaginationState;
199 sorting: MRT_SortingState;
200}) => {
201 return useQuery<UserApiResponse>({
202 queryKey: [
203 'users', //give a unique key for this query
204 columnFilters, //refetch when columnFilters changes
205 globalFilter, //refetch when globalFilter changes
206 pagination.pageIndex, //refetch when pagination.pageIndex changes
207 pagination.pageSize, //refetch when pagination.pageSize changes
208 sorting, //refetch when sorting changes
209 ],
210 queryFn: async () => {
211 const fetchURL = new URL('/api/data', location.origin);
212
213 //read our state and pass it to the API as query params
214 fetchURL.searchParams.set(
215 'start',
216 `${pagination.pageIndex * pagination.pageSize}`,
217 );
218 fetchURL.searchParams.set('size', `${pagination.pageSize}`);
219 fetchURL.searchParams.set('filters', JSON.stringify(columnFilters ?? []));
220 fetchURL.searchParams.set('globalFilter', globalFilter ?? '');
221 fetchURL.searchParams.set('sorting', JSON.stringify(sorting ?? []));
222
223 //use whatever fetch library you want, fetch, axios, etc
224 const response = await fetch(fetchURL.href);
225 const json = (await response.json()) as UserApiResponse;
226 return json;
227 },
228 placeholderData: keepPreviousData, //don't go to 0 rows when refetching or paginating to next page
229 });
230};
231
232//fetch more user info hook
233const useFetchUserInfo = (
234 params: { phoneNumber: string },
235 options: { enabled: boolean },
236) => {
237 return useQuery<FullUserInfoApiResponse>({
238 enabled: options.enabled, //only fetch when the detail panel is opened
239 staleTime: 60 * 1000, //don't refetch for 60 seconds
240 queryKey: ['user', params.phoneNumber], //give a unique key for this query for each user fetch
241 queryFn: async () => {
242 const fetchURL = new URL(
243 `/api/moredata/${params.phoneNumber
244 .replaceAll('-', '')
245 .replaceAll('.', '')
246 .replaceAll('(', '')
247 .replaceAll(')', '')}`,
248 location.origin,
249 );
250
251 //use whatever fetch library you want, fetch, axios, etc
252 const response = await fetch(fetchURL.href);
253 const json = (await response.json()) as FullUserInfoApiResponse;
254 return json;
255 },
256 });
257};
258

View Extra Storybook Examples