MRT logoMantine React Table

Async Loading Feature Guide

While you are fetching your data, you may want to show some loading indicators. Mantine 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
LoadingOverlayProps | ({ table }) => LoadingOverlayProps
Mantine LoadingOverlay Docs
2
ProgressProps | ({ isTopToolbar, table }) => ProgressProps
Mantine Progress Docs
3
SkeletonProps | ({ cell, column, row, table }) => SkeletonProps
Mantine Skeleton Docs

Relevant State Options

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

isLoading UI

Rather than coding your own spinner or loading indicator, you can simply set the isLoading state to true, and Mantine React Table will show a loading overlay with cell skeletons for you. The number of rows that get generated are the same as your initialState.pagination.pageSize option.
const table = useMantineReactTable({
columns,
data, //should fallback to empty array while loading data
state: { isLoading: true },
});
1-10 of 10
1
import { useMemo } from 'react';
2
import { MantineReactTable, type MRT_ColumnDef } from 'mantine-react-table';
3
import { Person } from './makeData';
4
5
const Example = () => {
6
const columns = useMemo<MRT_ColumnDef<Person>[]>(
7
() => [
8
{
9
accessorKey: 'firstName',
10
header: 'First Name',
11
},
12
{
13
accessorKey: 'lastName',
14
header: 'Last Name',
15
},
16
{
17
accessorKey: 'email',
18
header: 'Email',
19
},
20
{
21
accessorKey: 'city',
22
header: 'City',
23
},
24
],
25
[],
26
);
27
28
return (
29
<MantineReactTable
30
columns={columns}
31
data={[]}
32
state={{ isLoading: true }}
33
/>
34
);
35
};
36
37
export default Example;
1
import { useMemo } from 'react';
2
import { MantineReactTable } from 'mantine-react-table';
3
4
const Example = () => {
5
const columns = useMemo(
6
() => [
7
{
8
accessorKey: 'firstName',
9
header: 'First Name',
10
},
11
{
12
accessorKey: 'lastName',
13
header: 'Last Name',
14
},
15
{
16
accessorKey: 'email',
17
header: 'Email',
18
},
19
{
20
accessorKey: 'city',
21
header: 'City',
22
},
23
],
24
[],
25
);
26
27
return (
28
<MantineReactTable
29
columns={columns}
30
data={[]}
31
state={{ isLoading: true }}
32
/>
33
);
34
};
35
36
export default Example;

Show Loading Overlay, Skeletons, or Progress Bars Individually

You can control whether the loading overlay, skeletons, or progress bars show individually by setting the showLoadingOverlay, showSkeletons, and showProgressBars states to true.
const table = useMantineReactTable({
columns,
data: data ?? [],
state: {
//using react-query terminology as an example here
showLoadingOverlay: isFetching && isPreviousData, //fetching next page pagination
showSkeletons: isLoading, //loading for the first time with no data
showProgressBars: isSavingUser, //from a mutation
},
});

Customize Loading Components

You can customize the loading overlay, skeletons, and progress bars by passing props to the mantineLoadingOverlayProps, mantineSkeletonProps, and mantineProgressProps table options.
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
1
import { useEffect, useMemo, useState } from 'react';
2
import { MantineReactTable, type MRT_ColumnDef } from 'mantine-react-table';
3
import { data, type Person } from './makeData';
4
import { Button } from '@mantine/core';
5
6
const Example = () => {
7
const columns = useMemo<MRT_ColumnDef<Person>[]>(
8
() => [
9
{
10
accessorKey: 'firstName',
11
header: 'First Name',
12
},
13
{
14
accessorKey: 'lastName',
15
header: 'Last Name',
16
},
17
{
18
accessorKey: 'email',
19
header: 'Email',
20
},
21
{
22
accessorKey: 'city',
23
header: 'City',
24
},
25
],
26
[],
27
);
28
29
const [progress, setProgress] = useState(0);
30
31
//simulate random progress for demo purposes
32
useEffect(() => {
33
const interval = setInterval(() => {
34
setProgress((oldProgress) => {
35
const newProgress = Math.random() * 20;
36
return Math.min(oldProgress + newProgress, 100);
37
});
38
}, 1000);
39
return () => clearInterval(interval);
40
}, []);
41
42
return (
43
<MantineReactTable
44
columns={columns}
45
data={data}
46
mantineProgressProps={({ isTopToolbar }) => ({
47
color: 'orange',
48
variant: 'determinate', //if you want to show exact progress value
49
value: progress, //value between 0 and 100
50
sx: {
51
display: isTopToolbar ? 'block' : 'none', //hide bottom progress bar
52
},
53
})}
54
renderTopToolbarCustomActions={() => (
55
<Button onClick={() => setProgress(0)} variant="filled">
56
Reset
57
</Button>
58
)}
59
state={{ showProgressBars: true }}
60
/>
61
);
62
};
63
64
export default Example;
1
import { useEffect, useMemo, useState } from 'react';
2
import { MantineReactTable } from 'mantine-react-table';
3
import { data } from './makeData';
4
import { Button } from '@mantine/core';
5
6
const Example = () => {
7
const columns = useMemo(
8
() => [
9
{
10
accessorKey: 'firstName',
11
header: 'First Name',
12
},
13
{
14
accessorKey: 'lastName',
15
header: 'Last Name',
16
},
17
{
18
accessorKey: 'email',
19
header: 'Email',
20
},
21
{
22
accessorKey: 'city',
23
header: 'City',
24
},
25
],
26
[],
27
);
28
29
const [progress, setProgress] = useState(0);
30
31
//simulate random progress for demo purposes
32
useEffect(() => {
33
const interval = setInterval(() => {
34
setProgress((oldProgress) => {
35
const newProgress = Math.random() * 20;
36
return Math.min(oldProgress + newProgress, 100);
37
});
38
}, 1000);
39
return () => clearInterval(interval);
40
}, []);
41
42
return (
43
<MantineReactTable
44
columns={columns}
45
data={data}
46
mantineProgressProps={({ isTopToolbar }) => ({
47
color: 'orange',
48
variant: 'determinate', //if you want to show exact progress value
49
value: progress, //value between 0 and 100
50
sx: {
51
display: isTopToolbar ? 'block' : 'none', //hide bottom progress bar
52
},
53
})}
54
renderTopToolbarCustomActions={() => (
55
<Button onClick={() => setProgress(0)} variant="filled">
56
Reset
57
</Button>
58
)}
59
state={{ showProgressBars: true }}
60
/>
61
);
62
};
63
64
export default Example;

Full Loading and Server-Side Logic Example

Here is a copy of the full React Query example.
0-0 of 0
1
import { useMemo, useState } from 'react';
2
import {
3
MantineReactTable,
4
useMantineReactTable,
5
type MRT_ColumnDef,
6
type MRT_ColumnFiltersState,
7
type MRT_PaginationState,
8
type MRT_SortingState,
9
type MRT_ColumnFilterFnsState,
10
} from 'mantine-react-table';
11
import { ActionIcon, Tooltip } from '@mantine/core';
12
import { IconRefresh } from '@tabler/icons-react';
13
import {
14
QueryClient,
15
QueryClientProvider,
16
useQuery,
17
} from '@tanstack/react-query';
18
19
type User = {
20
firstName: string;
21
lastName: string;
22
address: string;
23
state: string;
24
phoneNumber: string;
25
};
26
27
type UserApiResponse = {
28
data: Array<User>;
29
meta: {
30
totalRowCount: number;
31
};
32
};
33
34
interface Params {
35
columnFilterFns: MRT_ColumnFilterFnsState;
36
columnFilters: MRT_ColumnFiltersState;
37
globalFilter: string;
38
sorting: MRT_SortingState;
39
pagination: MRT_PaginationState;
40
}
41
42
//custom react-query hook
43
const useGetUsers = ({
44
columnFilterFns,
45
columnFilters,
46
globalFilter,
47
sorting,
48
pagination,
49
}: Params) => {
50
//build the URL (https://www.mantine-react-table.com/api/data?start=0&size=10&filters=[]&globalFilter=&sorting=[])
51
const fetchURL = new URL(
52
'/api/data',
53
process.env.NODE_ENV === 'production'
54
? 'https://www.mantine-react-table.com'
55
: 'http://localhost:3001',
56
);
57
fetchURL.searchParams.set(
58
'start',
59
`${pagination.pageIndex * pagination.pageSize}`,
60
);
61
fetchURL.searchParams.set('size', `${pagination.pageSize}`);
62
fetchURL.searchParams.set('filters', JSON.stringify(columnFilters ?? []));
63
fetchURL.searchParams.set(
64
'filterModes',
65
JSON.stringify(columnFilterFns ?? {}),
66
);
67
fetchURL.searchParams.set('globalFilter', globalFilter ?? '');
68
fetchURL.searchParams.set('sorting', JSON.stringify(sorting ?? []));
69
70
return useQuery<UserApiResponse>({
71
queryKey: ['users', fetchURL.href], //refetch whenever the URL changes (columnFilters, globalFilter, sorting, pagination)
72
queryFn: () => fetch(fetchURL.href).then((res) => res.json()),
73
keepPreviousData: true, //useful for paginated queries by keeping data from previous pages on screen while fetching the next page
74
staleTime: 30_000, //don't refetch previously viewed pages until cache is more than 30 seconds old
75
});
76
};
77
78
const Example = () => {
79
const columns = useMemo<MRT_ColumnDef<User>[]>(
80
() => [
81
{
82
accessorKey: 'firstName',
83
header: 'First Name',
84
},
85
{
86
accessorKey: 'lastName',
87
header: 'Last Name',
88
},
89
{
90
accessorKey: 'address',
91
header: 'Address',
92
},
93
{
94
accessorKey: 'state',
95
header: 'State',
96
},
97
{
98
accessorKey: 'phoneNumber',
99
header: 'Phone Number',
100
},
101
],
102
[],
103
);
104
105
//Manage MRT state that we want to pass to our API
106
const [columnFilters, setColumnFilters] = useState<MRT_ColumnFiltersState>(
107
[],
108
);
109
const [columnFilterFns, setColumnFilterFns] = //filter modes
110
useState<MRT_ColumnFilterFnsState>(
111
Object.fromEntries(
112
columns.map(({ accessorKey }) => [accessorKey, 'contains']),
113
),
114
); //default to "contains" for all columns
115
const [globalFilter, setGlobalFilter] = useState('');
116
const [sorting, setSorting] = useState<MRT_SortingState>([]);
117
const [pagination, setPagination] = useState<MRT_PaginationState>({
118
pageIndex: 0,
119
pageSize: 10,
120
});
121
122
//call our custom react-query hook
123
const { data, isError, isFetching, isLoading, refetch } = useGetUsers({
124
columnFilterFns,
125
columnFilters,
126
globalFilter,
127
pagination,
128
sorting,
129
});
130
131
//this will depend on your API response shape
132
const fetchedUsers = data?.data ?? [];
133
const totalRowCount = data?.meta?.totalRowCount ?? 0;
134
135
const table = useMantineReactTable({
136
columns,
137
data: fetchedUsers,
138
enableColumnFilterModes: true,
139
columnFilterModeOptions: ['contains', 'startsWith', 'endsWith'],
140
initialState: { showColumnFilters: true },
141
manualFiltering: true,
142
manualPagination: true,
143
manualSorting: true,
144
mantineToolbarAlertBannerProps: isError
145
? {
146
color: 'red',
147
children: 'Error loading data',
148
}
149
: undefined,
150
onColumnFilterFnsChange: setColumnFilterFns,
151
onColumnFiltersChange: setColumnFilters,
152
onGlobalFilterChange: setGlobalFilter,
153
onPaginationChange: setPagination,
154
onSortingChange: setSorting,
155
renderTopToolbarCustomActions: () => (
156
<Tooltip label="Refresh Data">
157
<ActionIcon onClick={() => refetch()}>
158
<IconRefresh />
159
</ActionIcon>
160
</Tooltip>
161
),
162
rowCount: totalRowCount,
163
state: {
164
columnFilterFns,
165
columnFilters,
166
globalFilter,
167
isLoading,
168
pagination,
169
showAlertBanner: isError,
170
showProgressBars: isFetching,
171
sorting,
172
},
173
});
174
175
return <MantineReactTable table={table} />;
176
};
177
178
const queryClient = new QueryClient();
179
180
const ExampleWithReactQueryProvider = () => (
181
//Put this with your other react-query providers near root of your app
182
<QueryClientProvider client={queryClient}>
183
<Example />
184
</QueryClientProvider>
185
);
186
187
export default ExampleWithReactQueryProvider;
1
import { useMemo, useState } from 'react';
2
import { MantineReactTable, useMantineReactTable } from 'mantine-react-table';
3
import { ActionIcon, Tooltip } from '@mantine/core';
4
import { IconRefresh } from '@tabler/icons-react';
5
import {
6
QueryClient,
7
QueryClientProvider,
8
useQuery,
9
} from '@tanstack/react-query';
10
11
//custom react-query hook
12
const useGetUsers = ({
13
columnFilterFns,
14
columnFilters,
15
globalFilter,
16
sorting,
17
pagination,
18
}) => {
19
//build the URL (https://www.mantine-react-table.com/api/data?start=0&size=10&filters=[]&globalFilter=&sorting=[])
20
const fetchURL = new URL(
21
'/api/data',
22
process.env.NODE_ENV === 'production'
23
? 'https://www.mantine-react-table.com'
24
: 'http://localhost:3001',
25
);
26
fetchURL.searchParams.set(
27
'start',
28
`${pagination.pageIndex * pagination.pageSize}`,
29
);
30
fetchURL.searchParams.set('size', `${pagination.pageSize}`);
31
fetchURL.searchParams.set('filters', JSON.stringify(columnFilters ?? []));
32
fetchURL.searchParams.set(
33
'filterModes',
34
JSON.stringify(columnFilterFns ?? {}),
35
);
36
fetchURL.searchParams.set('globalFilter', globalFilter ?? '');
37
fetchURL.searchParams.set('sorting', JSON.stringify(sorting ?? []));
38
39
return useQuery({
40
queryKey: ['users', fetchURL.href], //refetch whenever the URL changes (columnFilters, globalFilter, sorting, pagination)
41
queryFn: () => fetch(fetchURL.href).then((res) => res.json()),
42
keepPreviousData: true, //useful for paginated queries by keeping data from previous pages on screen while fetching the next page
43
staleTime: 30_000, //don't refetch previously viewed pages until cache is more than 30 seconds old
44
});
45
};
46
47
const Example = () => {
48
const columns = useMemo(
49
() => [
50
{
51
accessorKey: 'firstName',
52
header: 'First Name',
53
},
54
{
55
accessorKey: 'lastName',
56
header: 'Last Name',
57
},
58
{
59
accessorKey: 'address',
60
header: 'Address',
61
},
62
{
63
accessorKey: 'state',
64
header: 'State',
65
},
66
{
67
accessorKey: 'phoneNumber',
68
header: 'Phone Number',
69
},
70
],
71
[],
72
);
73
74
//Manage MRT state that we want to pass to our API
75
const [columnFilters, setColumnFilters] = useState([]);
76
const [columnFilterFns, setColumnFilterFns] = //filter modes
77
useState(
78
Object.fromEntries(
79
columns.map(({ accessorKey }) => [accessorKey, 'contains']),
80
),
81
); //default to "contains" for all columns
82
const [globalFilter, setGlobalFilter] = useState('');
83
const [sorting, setSorting] = useState([]);
84
const [pagination, setPagination] = useState({
85
pageIndex: 0,
86
pageSize: 10,
87
});
88
89
//call our custom react-query hook
90
const { data, isError, isFetching, isLoading, refetch } = useGetUsers({
91
columnFilterFns,
92
columnFilters,
93
globalFilter,
94
pagination,
95
sorting,
96
});
97
98
//this will depend on your API response shape
99
const fetchedUsers = data?.data ?? [];
100
const totalRowCount = data?.meta?.totalRowCount ?? 0;
101
102
const table = useMantineReactTable({
103
columns,
104
data: fetchedUsers,
105
enableColumnFilterModes: true,
106
columnFilterModeOptions: ['contains', 'startsWith', 'endsWith'],
107
initialState: { showColumnFilters: true },
108
manualFiltering: true,
109
manualPagination: true,
110
manualSorting: true,
111
mantineToolbarAlertBannerProps: isError
112
? {
113
color: 'red',
114
children: 'Error loading data',
115
}
116
: undefined,
117
onColumnFilterFnsChange: setColumnFilterFns,
118
onColumnFiltersChange: setColumnFilters,
119
onGlobalFilterChange: setGlobalFilter,
120
onPaginationChange: setPagination,
121
onSortingChange: setSorting,
122
renderTopToolbarCustomActions: () => (
123
<Tooltip label="Refresh Data">
124
<ActionIcon onClick={() => refetch()}>
125
<IconRefresh />
126
</ActionIcon>
127
</Tooltip>
128
),
129
rowCount: totalRowCount,
130
state: {
131
columnFilterFns,
132
columnFilters,
133
globalFilter,
134
isLoading,
135
pagination,
136
showAlertBanner: isError,
137
showProgressBars: isFetching,
138
sorting,
139
},
140
});
141
142
return <MantineReactTable table={table} />;
143
};
144
145
const queryClient = new QueryClient();
146
147
const ExampleWithReactQueryProvider = () => (
148
//Put this with your other react-query providers near root of your app
149
<QueryClientProvider client={queryClient}>
150
<Example />
151
</QueryClientProvider>
152
);
153
154
export default ExampleWithReactQueryProvider;
1
import {
2
type MRT_ColumnFiltersState,
3
type MRT_SortingState,
4
} from 'mantine-react-table';
5
import { type NextApiRequest, type NextApiResponse } from 'next';
6
import { getData } from './mock';
7
8
//This is just a simple mock of a backend API where you would do server-side pagination, filtering, and sorting
9
//You would most likely want way more validation and error handling than this in a real world application
10
//Also most of this logic should actually be in the database query itself, but this is just a mock
11
export default function handler(req: NextApiRequest, res: NextApiResponse) {
12
let dbData = getData();
13
const { start, size, filters, filterModes, sorting, globalFilter } =
14
req.query as Record<string, string>;
15
16
const parsedFilterModes = JSON.parse(filterModes ?? '{}') as Record<
17
string,
18
string
19
>;
20
21
const parsedColumnFilters = JSON.parse(filters) as MRT_ColumnFiltersState;
22
if (parsedColumnFilters?.length) {
23
parsedColumnFilters.map((filter) => {
24
const { id: columnId, value: filterValue } = filter;
25
const filterMode = parsedFilterModes?.[columnId] ?? 'contains';
26
dbData = dbData.filter((row) => {
27
const rowValue = row[columnId]?.toString()?.toLowerCase();
28
if (filterMode === 'contains') {
29
return rowValue.includes?.((filterValue as string).toLowerCase());
30
} else if (filterMode === 'startsWith') {
31
return rowValue.startsWith?.((filterValue as string).toLowerCase());
32
} else if (filterMode === 'endsWith') {
33
return rowValue.endsWith?.((filterValue as string).toLowerCase());
34
}
35
});
36
});
37
}
38
39
if (globalFilter) {
40
dbData = dbData.filter((row) =>
41
Object.keys(row).some(
42
(columnId) =>
43
row[columnId]
44
?.toString()
45
?.toLowerCase()
46
?.includes?.((globalFilter as string).toLowerCase()),
47
),
48
);
49
}
50
51
const parsedSorting = JSON.parse(sorting) as MRT_SortingState;
52
if (parsedSorting?.length) {
53
const sort = parsedSorting[0];
54
const { id, desc } = sort;
55
dbData.sort((a, b) => {
56
if (desc) {
57
return a[id] < b[id] ? 1 : -1;
58
}
59
return a[id] > b[id] ? 1 : -1;
60
});
61
}
62
63
res.status(200).json({
64
data:
65
dbData?.slice(parseInt(start), parseInt(start) + parseInt(size)) ?? [],
66
meta: { totalRowCount: dbData.length },
67
});
68
}
You can help make these docs better! PRs are Welcome
Using Material-UI instead of Mantine?
Check out Material React Table