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
# | Prop Name | Type | Default Value | More Info Links | |
---|---|---|---|---|---|
1 |
| Mantine LoadingOverlay Docs | |||
No Description Provided... Yet... | |||||
2 |
| Mantine Progress Docs | |||
No Description Provided... Yet... | |||||
3 |
| Mantine Skeleton Docs | |||
No Description Provided... Yet... |
Relevant State Options
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 datastate: { isLoading: true },});
First Name | Last Name | Email | City |
---|---|---|---|
1-10 of 10
1import { useMemo } from 'react';2import { MantineReactTable, type MRT_ColumnDef } from 'mantine-react-table';3import { Person } from './makeData';45const Example = () => {6const columns = useMemo<MRT_ColumnDef<Person>[]>(7() => [8{9accessorKey: 'firstName',10header: 'First Name',11},12{13accessorKey: 'lastName',14header: 'Last Name',15},16{17accessorKey: 'email',18header: 'Email',19},20{21accessorKey: 'city',22header: 'City',23},24],25[],26);2728return (29<MantineReactTable30columns={columns}31data={[]}32state={{ isLoading: true }}33/>34);35};3637export default Example;
1import { useMemo } from 'react';2import { MantineReactTable } from 'mantine-react-table';34const Example = () => {5const columns = useMemo(6() => [7{8accessorKey: 'firstName',9header: 'First Name',10},11{12accessorKey: 'lastName',13header: 'Last Name',14},15{16accessorKey: 'email',17header: 'Email',18},19{20accessorKey: 'city',21header: 'City',22},23],24[],25);2627return (28<MantineReactTable29columns={columns}30data={[]}31state={{ isLoading: true }}32/>33);34};3536export 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 hereshowLoadingOverlay: isFetching && isPreviousData, //fetching next page paginationshowSkeletons: isLoading, //loading for the first time with no datashowProgressBars: 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.First Name | Last Name | Email | City |
---|---|---|---|
Dylan | Murray | dmurray@yopmail.com | East Daphne |
Raquel | Kohler | rkholer33@yopmail.com | Columbus |
Ervin | Reinger | ereinger@mailinator.com | South Linda |
Brittany | McCullough | bmccullough44@mailinator.com | Lincoln |
Branson | Frami | bframi@yopmain.com | New York |
Kevin | Klein | kklien@mailinator.com | Nebraska |
1-6 of 6
1import { useEffect, useMemo, useState } from 'react';2import { MantineReactTable, type MRT_ColumnDef } from 'mantine-react-table';3import { data, type Person } from './makeData';4import { Button } from '@mantine/core';56const Example = () => {7const columns = useMemo<MRT_ColumnDef<Person>[]>(8() => [9{10accessorKey: 'firstName',11header: 'First Name',12},13{14accessorKey: 'lastName',15header: 'Last Name',16},17{18accessorKey: 'email',19header: 'Email',20},21{22accessorKey: 'city',23header: 'City',24},25],26[],27);2829const [progress, setProgress] = useState(0);3031//simulate random progress for demo purposes32useEffect(() => {33const interval = setInterval(() => {34setProgress((oldProgress) => {35const newProgress = Math.random() * 20;36return Math.min(oldProgress + newProgress, 100);37});38}, 1000);39return () => clearInterval(interval);40}, []);4142return (43<MantineReactTable44columns={columns}45data={data}46mantineProgressProps={({ isTopToolbar }) => ({47color: 'orange',48variant: 'determinate', //if you want to show exact progress value49value: progress, //value between 0 and 10050sx: {51display: isTopToolbar ? 'block' : 'none', //hide bottom progress bar52},53})}54renderTopToolbarCustomActions={() => (55<Button onClick={() => setProgress(0)} variant="filled">56Reset57</Button>58)}59state={{ showProgressBars: true }}60/>61);62};6364export default Example;
1import { useEffect, useMemo, useState } from 'react';2import { MantineReactTable } from 'mantine-react-table';3import { data } from './makeData';4import { Button } from '@mantine/core';56const Example = () => {7const columns = useMemo(8() => [9{10accessorKey: 'firstName',11header: 'First Name',12},13{14accessorKey: 'lastName',15header: 'Last Name',16},17{18accessorKey: 'email',19header: 'Email',20},21{22accessorKey: 'city',23header: 'City',24},25],26[],27);2829const [progress, setProgress] = useState(0);3031//simulate random progress for demo purposes32useEffect(() => {33const interval = setInterval(() => {34setProgress((oldProgress) => {35const newProgress = Math.random() * 20;36return Math.min(oldProgress + newProgress, 100);37});38}, 1000);39return () => clearInterval(interval);40}, []);4142return (43<MantineReactTable44columns={columns}45data={data}46mantineProgressProps={({ isTopToolbar }) => ({47color: 'orange',48variant: 'determinate', //if you want to show exact progress value49value: progress, //value between 0 and 10050sx: {51display: isTopToolbar ? 'block' : 'none', //hide bottom progress bar52},53})}54renderTopToolbarCustomActions={() => (55<Button onClick={() => setProgress(0)} variant="filled">56Reset57</Button>58)}59state={{ showProgressBars: true }}60/>61);62};6364export default Example;
Full Loading and Server-Side Logic Example
Here is a copy of the full React Query example.
First Name | Last Name | Address | State | Phone Number |
---|---|---|---|---|
0-0 of 0
1import { useMemo, useState } from 'react';2import {3MantineReactTable,4useMantineReactTable,5type MRT_ColumnDef,6type MRT_ColumnFiltersState,7type MRT_PaginationState,8type MRT_SortingState,9type MRT_ColumnFilterFnsState,10} from 'mantine-react-table';11import { ActionIcon, Tooltip } from '@mantine/core';12import { IconRefresh } from '@tabler/icons-react';13import {14QueryClient,15QueryClientProvider,16useQuery,17} from '@tanstack/react-query';1819type User = {20firstName: string;21lastName: string;22address: string;23state: string;24phoneNumber: string;25};2627type UserApiResponse = {28data: Array<User>;29meta: {30totalRowCount: number;31};32};3334interface Params {35columnFilterFns: MRT_ColumnFilterFnsState;36columnFilters: MRT_ColumnFiltersState;37globalFilter: string;38sorting: MRT_SortingState;39pagination: MRT_PaginationState;40}4142//custom react-query hook43const useGetUsers = ({44columnFilterFns,45columnFilters,46globalFilter,47sorting,48pagination,49}: Params) => {50//build the URL (https://www.mantine-react-table.com/api/data?start=0&size=10&filters=[]&globalFilter=&sorting=[])51const fetchURL = new URL(52'/api/data',53process.env.NODE_ENV === 'production'54? 'https://www.mantine-react-table.com'55: 'http://localhost:3001',56);57fetchURL.searchParams.set(58'start',59`${pagination.pageIndex * pagination.pageSize}`,60);61fetchURL.searchParams.set('size', `${pagination.pageSize}`);62fetchURL.searchParams.set('filters', JSON.stringify(columnFilters ?? []));63fetchURL.searchParams.set(64'filterModes',65JSON.stringify(columnFilterFns ?? {}),66);67fetchURL.searchParams.set('globalFilter', globalFilter ?? '');68fetchURL.searchParams.set('sorting', JSON.stringify(sorting ?? []));6970return useQuery<UserApiResponse>({71queryKey: ['users', fetchURL.href], //refetch whenever the URL changes (columnFilters, globalFilter, sorting, pagination)72queryFn: () => fetch(fetchURL.href).then((res) => res.json()),73keepPreviousData: true, //useful for paginated queries by keeping data from previous pages on screen while fetching the next page74staleTime: 30_000, //don't refetch previously viewed pages until cache is more than 30 seconds old75});76};7778const Example = () => {79const columns = useMemo<MRT_ColumnDef<User>[]>(80() => [81{82accessorKey: 'firstName',83header: 'First Name',84},85{86accessorKey: 'lastName',87header: 'Last Name',88},89{90accessorKey: 'address',91header: 'Address',92},93{94accessorKey: 'state',95header: 'State',96},97{98accessorKey: 'phoneNumber',99header: 'Phone Number',100},101],102[],103);104105//Manage MRT state that we want to pass to our API106const [columnFilters, setColumnFilters] = useState<MRT_ColumnFiltersState>(107[],108);109const [columnFilterFns, setColumnFilterFns] = //filter modes110useState<MRT_ColumnFilterFnsState>(111Object.fromEntries(112columns.map(({ accessorKey }) => [accessorKey, 'contains']),113),114); //default to "contains" for all columns115const [globalFilter, setGlobalFilter] = useState('');116const [sorting, setSorting] = useState<MRT_SortingState>([]);117const [pagination, setPagination] = useState<MRT_PaginationState>({118pageIndex: 0,119pageSize: 10,120});121122//call our custom react-query hook123const { data, isError, isFetching, isLoading, refetch } = useGetUsers({124columnFilterFns,125columnFilters,126globalFilter,127pagination,128sorting,129});130131//this will depend on your API response shape132const fetchedUsers = data?.data ?? [];133const totalRowCount = data?.meta?.totalRowCount ?? 0;134135const table = useMantineReactTable({136columns,137data: fetchedUsers,138enableColumnFilterModes: true,139columnFilterModeOptions: ['contains', 'startsWith', 'endsWith'],140initialState: { showColumnFilters: true },141manualFiltering: true,142manualPagination: true,143manualSorting: true,144mantineToolbarAlertBannerProps: isError145? {146color: 'red',147children: 'Error loading data',148}149: undefined,150onColumnFilterFnsChange: setColumnFilterFns,151onColumnFiltersChange: setColumnFilters,152onGlobalFilterChange: setGlobalFilter,153onPaginationChange: setPagination,154onSortingChange: setSorting,155renderTopToolbarCustomActions: () => (156<Tooltip label="Refresh Data">157<ActionIcon onClick={() => refetch()}>158<IconRefresh />159</ActionIcon>160</Tooltip>161),162rowCount: totalRowCount,163state: {164columnFilterFns,165columnFilters,166globalFilter,167isLoading,168pagination,169showAlertBanner: isError,170showProgressBars: isFetching,171sorting,172},173});174175return <MantineReactTable table={table} />;176};177178const queryClient = new QueryClient();179180const ExampleWithReactQueryProvider = () => (181//Put this with your other react-query providers near root of your app182<QueryClientProvider client={queryClient}>183<Example />184</QueryClientProvider>185);186187export default ExampleWithReactQueryProvider;
1import { useMemo, useState } from 'react';2import { MantineReactTable, useMantineReactTable } from 'mantine-react-table';3import { ActionIcon, Tooltip } from '@mantine/core';4import { IconRefresh } from '@tabler/icons-react';5import {6QueryClient,7QueryClientProvider,8useQuery,9} from '@tanstack/react-query';1011//custom react-query hook12const useGetUsers = ({13columnFilterFns,14columnFilters,15globalFilter,16sorting,17pagination,18}) => {19//build the URL (https://www.mantine-react-table.com/api/data?start=0&size=10&filters=[]&globalFilter=&sorting=[])20const fetchURL = new URL(21'/api/data',22process.env.NODE_ENV === 'production'23? 'https://www.mantine-react-table.com'24: 'http://localhost:3001',25);26fetchURL.searchParams.set(27'start',28`${pagination.pageIndex * pagination.pageSize}`,29);30fetchURL.searchParams.set('size', `${pagination.pageSize}`);31fetchURL.searchParams.set('filters', JSON.stringify(columnFilters ?? []));32fetchURL.searchParams.set(33'filterModes',34JSON.stringify(columnFilterFns ?? {}),35);36fetchURL.searchParams.set('globalFilter', globalFilter ?? '');37fetchURL.searchParams.set('sorting', JSON.stringify(sorting ?? []));3839return useQuery({40queryKey: ['users', fetchURL.href], //refetch whenever the URL changes (columnFilters, globalFilter, sorting, pagination)41queryFn: () => fetch(fetchURL.href).then((res) => res.json()),42keepPreviousData: true, //useful for paginated queries by keeping data from previous pages on screen while fetching the next page43staleTime: 30_000, //don't refetch previously viewed pages until cache is more than 30 seconds old44});45};4647const Example = () => {48const columns = useMemo(49() => [50{51accessorKey: 'firstName',52header: 'First Name',53},54{55accessorKey: 'lastName',56header: 'Last Name',57},58{59accessorKey: 'address',60header: 'Address',61},62{63accessorKey: 'state',64header: 'State',65},66{67accessorKey: 'phoneNumber',68header: 'Phone Number',69},70],71[],72);7374//Manage MRT state that we want to pass to our API75const [columnFilters, setColumnFilters] = useState([]);76const [columnFilterFns, setColumnFilterFns] = //filter modes77useState(78Object.fromEntries(79columns.map(({ accessorKey }) => [accessorKey, 'contains']),80),81); //default to "contains" for all columns82const [globalFilter, setGlobalFilter] = useState('');83const [sorting, setSorting] = useState([]);84const [pagination, setPagination] = useState({85pageIndex: 0,86pageSize: 10,87});8889//call our custom react-query hook90const { data, isError, isFetching, isLoading, refetch } = useGetUsers({91columnFilterFns,92columnFilters,93globalFilter,94pagination,95sorting,96});9798//this will depend on your API response shape99const fetchedUsers = data?.data ?? [];100const totalRowCount = data?.meta?.totalRowCount ?? 0;101102const table = useMantineReactTable({103columns,104data: fetchedUsers,105enableColumnFilterModes: true,106columnFilterModeOptions: ['contains', 'startsWith', 'endsWith'],107initialState: { showColumnFilters: true },108manualFiltering: true,109manualPagination: true,110manualSorting: true,111mantineToolbarAlertBannerProps: isError112? {113color: 'red',114children: 'Error loading data',115}116: undefined,117onColumnFilterFnsChange: setColumnFilterFns,118onColumnFiltersChange: setColumnFilters,119onGlobalFilterChange: setGlobalFilter,120onPaginationChange: setPagination,121onSortingChange: setSorting,122renderTopToolbarCustomActions: () => (123<Tooltip label="Refresh Data">124<ActionIcon onClick={() => refetch()}>125<IconRefresh />126</ActionIcon>127</Tooltip>128),129rowCount: totalRowCount,130state: {131columnFilterFns,132columnFilters,133globalFilter,134isLoading,135pagination,136showAlertBanner: isError,137showProgressBars: isFetching,138sorting,139},140});141142return <MantineReactTable table={table} />;143};144145const queryClient = new QueryClient();146147const ExampleWithReactQueryProvider = () => (148//Put this with your other react-query providers near root of your app149<QueryClientProvider client={queryClient}>150<Example />151</QueryClientProvider>152);153154export default ExampleWithReactQueryProvider;
1import {2type MRT_ColumnFiltersState,3type MRT_SortingState,4} from 'mantine-react-table';5import { type NextApiRequest, type NextApiResponse } from 'next';6import { getData } from './mock';78//This is just a simple mock of a backend API where you would do server-side pagination, filtering, and sorting9//You would most likely want way more validation and error handling than this in a real world application10//Also most of this logic should actually be in the database query itself, but this is just a mock11export default function handler(req: NextApiRequest, res: NextApiResponse) {12let dbData = getData();13const { start, size, filters, filterModes, sorting, globalFilter } =14req.query as Record<string, string>;1516const parsedFilterModes = JSON.parse(filterModes ?? '{}') as Record<17string,18string19>;2021const parsedColumnFilters = JSON.parse(filters) as MRT_ColumnFiltersState;22if (parsedColumnFilters?.length) {23parsedColumnFilters.map((filter) => {24const { id: columnId, value: filterValue } = filter;25const filterMode = parsedFilterModes?.[columnId] ?? 'contains';26dbData = dbData.filter((row) => {27const rowValue = row[columnId]?.toString()?.toLowerCase();28if (filterMode === 'contains') {29return rowValue.includes?.((filterValue as string).toLowerCase());30} else if (filterMode === 'startsWith') {31return rowValue.startsWith?.((filterValue as string).toLowerCase());32} else if (filterMode === 'endsWith') {33return rowValue.endsWith?.((filterValue as string).toLowerCase());34}35});36});37}3839if (globalFilter) {40dbData = dbData.filter((row) =>41Object.keys(row).some(42(columnId) =>43row[columnId]44?.toString()45?.toLowerCase()46?.includes?.((globalFilter as string).toLowerCase()),47),48);49}5051const parsedSorting = JSON.parse(sorting) as MRT_SortingState;52if (parsedSorting?.length) {53const sort = parsedSorting[0];54const { id, desc } = sort;55dbData.sort((a, b) => {56if (desc) {57return a[id] < b[id] ? 1 : -1;58}59return a[id] > b[id] ? 1 : -1;60});61}6263res.status(200).json({64data:65dbData?.slice(parseInt(start), parseInt(start) + parseInt(size)) ?? [],66meta: { totalRowCount: dbData.length },67});68}