MRT logoMaterial React Table

Async Loading UI Feature Guide

While you are fetching your data, you may want to show some loading indicators. Material React Table has some nice loading UI features built in that look better than a simple spinner.

This guide is mostly focused on the loading UI features. Make sure to also check out the Remote Data and React Query examples for server-side logic examples.

Relevant Table Options

1
CircularProgressProps | ({ table }) => CircularProgressProps
Material UI CircularProgress Props
2
LinearProgressProps | ({ isTopToolbar, table }) => LinearProgressProps
Material UI LinearProgress Props
3
SkeletonProps | ({ cell, column, row, table }) => SkeletonProps
Material UI Skeleton Props

Relevant State Options

1
boolean
false
2
boolean
false
3
boolean
false
4
boolean
false
5
boolean
false

isLoading UI

There are three different loading UI features that are built into Material React Table:

  1. Loading Overlay - shows spinner overlay over the table container. (New in V2)

  2. Cell Skeletons - show pretty and shimmering skeletons for each cell.

  3. Linear Progress Bars - shows progress bars above and/or below the table.

You can use any combination of these loading UIs by managing the showLoadingOverlay, showSkeletons, and showProgressBars states.

There are also two other loading states that are shortcuts for combining some of the above states:

  • isLoading - shows loading overlay and cell skeletons.

  • isSaving - shows the progress bars and adds spinners to the save buttons in editing features.

Here is some of the recommended loading UI that you might use with React Query:

const {
data = [],
isLoading: isLoadingTodos,
isRefetching: isRefetchingTodos,
} = useQuery({
/**/
});
const { mutate, isPending: isSavingTodos } = useMutation({
/**/
});
const table = useMaterialReactTable({
columns,
data,
state: {
isLoading: isLoadingTodos, //cell skeletons and loading overlay
showProgressBars: isRefetchingTodos, //progress bars while refetching
isSaving: isSavingTodos, //progress bars and save button spinners
},
});
return <MaterialReactTable table={table} />;

Note: The Loading Overlay UI makes the table container non-interactive while it is showing. This is usually desired while no data is yet in the table. Consider avoiding using the Loading Overlay UI during "refetching" operations like filtering, sorting, or pagination.

Customize Loading UI

You can customize the loading UI by passing props to the muiSkeletonProps, muiLinearProgressProps, and muiCircularProgressProps props.

const table = useMaterialReactTable({
columns,
data,
muiSkeletonProps: {
animation: 'wave',
},
muiLinearProgressProps: {
color: 'secondary',
},
muiCircularProgressProps: {
color: 'secondary',
},
});
return <MaterialReactTable table={table} />;
1-10 of 10

Source Code

1import { useMemo } from 'react';
2import { MaterialReactTable, type MRT_ColumnDef } from 'material-react-table';
3import { type Person } from './makeData';
4
5const data: Array<Person> = [];
6
7const Example = () => {
8 const columns = useMemo<MRT_ColumnDef<Person>[]>(
9 //column definitions...
30 );
31
32 return (
33 <MaterialReactTable
34 columns={columns}
35 data={data}
36 state={{ isLoading: true }}
37 muiCircularProgressProps={{
38 color: 'secondary',
39 thickness: 5,
40 size: 55,
41 }}
42 muiSkeletonProps={{
43 animation: 'pulse',
44 height: 28,
45 }}
46 />
47 );
48};
49
50export default Example;
51

Only Show Progress Bars or Skeletons

If you do not want both progress bars and cell skeletons to show, you can use the showProgressBars and showSkeletons states, instead.

const table = useMaterialReactTable({
columns,
data,
state: {
showProgressBars: true, //or showSkeletons
},
});
DylanMurraydmurray@yopmail.comEast Daphne
RaquelKohlerrkholer33@yopmail.comColumbus
ErvinReingerereinger@mailinator.comSouth Linda
BrittanyMcCulloughbmccullough44@mailinator.comLincoln
BransonFramibframi@yopmain.comNew York
KevinKleinkklien@mailinator.comNebraska
1-6 of 6

Source Code

1import { useEffect, useMemo, useState } from 'react';
2import { MaterialReactTable, type MRT_ColumnDef } from 'material-react-table';
3import { data, type Person } from './makeData';
4import { Button } from '@mui/material';
5
6const Example = () => {
7 const columns = useMemo<MRT_ColumnDef<Person>[]>(
8 //column definitions...
29 );
30
31 const [progress, setProgress] = useState(0);
32
33 //simulate random progress for demo purposes
34 useEffect(() => {
35 const interval = setInterval(() => {
36 setProgress((oldProgress) => {
37 const newProgress = Math.random() * 20;
38 return Math.min(oldProgress + newProgress, 100);
39 });
40 }, 1000);
41 return () => clearInterval(interval);
42 }, []);
43
44 return (
45 <MaterialReactTable
46 columns={columns}
47 data={data}
48 muiLinearProgressProps={({ isTopToolbar }) => ({
49 color: 'secondary',
50 variant: 'determinate', //if you want to show exact progress value
51 value: progress, //value between 0 and 100
52 sx: {
53 display: isTopToolbar ? 'block' : 'none', //hide bottom progress bar
54 },
55 })}
56 renderTopToolbarCustomActions={() => (
57 <Button onClick={() => setProgress(0)} variant="contained">
58 Reset
59 </Button>
60 )}
61 state={{ showProgressBars: true }}
62 />
63 );
64};
65
66export default Example;
67

Full Loading and Server-Side Logic Example

Here is a copy of the full React Query example.

0-0 of 0

Source Code

1import { 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} from 'material-react-table';
10import { IconButton, Tooltip } from '@mui/material';
11import RefreshIcon from '@mui/icons-material/Refresh';
12import {
13 QueryClient,
14 QueryClientProvider,
15 keepPreviousData,
16 useQuery,
17} from '@tanstack/react-query'; //note: this is TanStack React Query V5
18
19//Your API response shape will probably be different. Knowing a total row count is important though.
20type UserApiResponse = {
21 data: Array<User>;
22 meta: {
23 totalRowCount: number;
24 };
25};
26
27type User = {
28 firstName: string;
29 lastName: string;
30 address: string;
31 state: string;
32 phoneNumber: string;
33 lastLogin: Date;
34};
35
36const Example = () => {
37 //manage our own state for stuff we want to pass to the API
38 const [columnFilters, setColumnFilters] = useState<MRT_ColumnFiltersState>(
39 [],
40 );
41 const [globalFilter, setGlobalFilter] = useState('');
42 const [sorting, setSorting] = useState<MRT_SortingState>([]);
43 const [pagination, setPagination] = useState<MRT_PaginationState>({
44 pageIndex: 0,
45 pageSize: 10,
46 });
47
48 //consider storing this code in a custom hook (i.e useFetchUsers)
49 const {
50 data: { data = [], meta } = {}, //your data and api response will probably be different
51 isError,
52 isRefetching,
53 isLoading,
54 refetch,
55 } = useQuery<UserApiResponse>({
56 queryKey: [
57 'table-data',
58 columnFilters, //refetch when columnFilters changes
59 globalFilter, //refetch when globalFilter changes
60 pagination.pageIndex, //refetch when pagination.pageIndex changes
61 pagination.pageSize, //refetch when pagination.pageSize changes
62 sorting, //refetch when sorting changes
63 ],
64 queryFn: async () => {
65 const fetchURL = new URL(
66 '/api/data',
67 process.env.NODE_ENV === 'production'
68 ? 'https://www.material-react-table.com'
69 : 'http://localhost:3000',
70 );
71
72 //read our state and pass it to the API as query params
73 fetchURL.searchParams.set(
74 'start',
75 `${pagination.pageIndex * pagination.pageSize}`,
76 );
77 fetchURL.searchParams.set('size', `${pagination.pageSize}`);
78 fetchURL.searchParams.set('filters', JSON.stringify(columnFilters ?? []));
79 fetchURL.searchParams.set('globalFilter', globalFilter ?? '');
80 fetchURL.searchParams.set('sorting', JSON.stringify(sorting ?? []));
81
82 //use whatever fetch library you want, fetch, axios, etc
83 const response = await fetch(fetchURL.href);
84 const json = (await response.json()) as UserApiResponse;
85 return json;
86 },
87 placeholderData: keepPreviousData, //don't go to 0 rows when refetching or paginating to next page
88 });
89
90 const columns = useMemo<MRT_ColumnDef<User>[]>(
91 //column definitions...
125 );
126
127 const table = useMaterialReactTable({
128 columns,
129 data,
130 initialState: { showColumnFilters: true },
131 manualFiltering: true, //turn off built-in client-side filtering
132 manualPagination: true, //turn off built-in client-side pagination
133 manualSorting: true, //turn off built-in client-side sorting
134 muiToolbarAlertBannerProps: isError
135 ? {
136 color: 'error',
137 children: 'Error loading data',
138 }
139 : undefined,
140 onColumnFiltersChange: setColumnFilters,
141 onGlobalFilterChange: setGlobalFilter,
142 onPaginationChange: setPagination,
143 onSortingChange: setSorting,
144 renderTopToolbarCustomActions: () => (
145 <Tooltip arrow title="Refresh Data">
146 <IconButton onClick={() => refetch()}>
147 <RefreshIcon />
148 </IconButton>
149 </Tooltip>
150 ),
151 rowCount: meta?.totalRowCount ?? 0,
152 state: {
153 columnFilters,
154 globalFilter,
155 isLoading,
156 pagination,
157 showAlertBanner: isError,
158 showProgressBars: isRefetching,
159 sorting,
160 },
161 });
162
163 return <MaterialReactTable table={table} />;
164};
165
166const queryClient = new QueryClient();
167
168const ExampleWithReactQueryProvider = () => (
169 //App.tsx or AppProviders file. Don't just wrap this component with QueryClientProvider! Wrap your whole App!
170 <QueryClientProvider client={queryClient}>
171 <Example />
172 </QueryClientProvider>
173);
174
175export default ExampleWithReactQueryProvider;
176