MRT logoMantine React Table

On This Page

    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