Infinite Scrolling Example
An infinite scrolling table is a table that streams data from a remote server as the user scrolls down the table. This works great with large datasets, just like our Virtualized Example, except here we do not fetch all of the data at once upfront. Instead, we just fetch data a little bit at a time, as it becomes necessary.
Using a library like
@tanstack/react-query
makes it easy to implement an infinite scrolling table in Mantine React Table with the useInfiniteQuery
hook.Enabling the virtualization feature is actually optional here but is encouraged if the table will be expected to render more than 100 rows at a time.
# | First Name | Last Name | Address | State | Phone Number |
---|
Fetched 0 of 0 total rows.
1import {2type UIEvent,3useCallback,4useEffect,5useMemo,6useRef,7useState,8} from 'react';9import {10MantineReactTable,11useMantineReactTable,12type MRT_ColumnDef,13type MRT_ColumnFiltersState,14type MRT_SortingState,15type MRT_Virtualizer,16} from 'mantine-react-table';17import { Text } from '@mantine/core';18import {19QueryClient,20QueryClientProvider,21useInfiniteQuery,22} from '@tanstack/react-query';2324type UserApiResponse = {25data: Array<User>;26meta: {27totalRowCount: number;28};29};3031type User = {32firstName: string;33lastName: string;34address: string;35state: string;36phoneNumber: string;37};3839const columns: MRT_ColumnDef<User>[] = [40{41accessorKey: 'firstName',42header: 'First Name',43},44{45accessorKey: 'lastName',46header: 'Last Name',47},48{49accessorKey: 'address',50header: 'Address',51},52{53accessorKey: 'state',54header: 'State',55},56{57accessorKey: 'phoneNumber',58header: 'Phone Number',59},60];6162const fetchSize = 25;6364const Example = () => {65const tableContainerRef = useRef<HTMLDivElement>(null); //we can get access to the underlying TableContainer element and react to its scroll events66const rowVirtualizerInstanceRef =67useRef<MRT_Virtualizer<HTMLDivElement, HTMLTableRowElement>>(null); //we can get access to the underlying Virtualizer instance and call its scrollToIndex method6869const [columnFilters, setColumnFilters] = useState<MRT_ColumnFiltersState>(70[],71);72const [globalFilter, setGlobalFilter] = useState<string>();73const [sorting, setSorting] = useState<MRT_SortingState>([]);7475const { data, fetchNextPage, isError, isFetching, isLoading } =76useInfiniteQuery<UserApiResponse>({77queryKey: ['table-data', columnFilters, globalFilter, sorting],78queryFn: async ({ pageParam = 0 }) => {79const url = new URL(80'/api/data',81process.env.NODE_ENV === 'production'82? 'https://www.mantine-react-table.com'83: 'http://localhost:3001',84);85url.searchParams.set('start', `${pageParam * fetchSize}`);86url.searchParams.set('size', `${fetchSize}`);87url.searchParams.set('filters', JSON.stringify(columnFilters ?? []));88url.searchParams.set('globalFilter', globalFilter ?? '');89url.searchParams.set('sorting', JSON.stringify(sorting ?? []));9091const response = await fetch(url.href);92const json = (await response.json()) as UserApiResponse;93return json;94},95getNextPageParam: (_lastGroup, groups) => groups.length,96keepPreviousData: true,97refetchOnWindowFocus: false,98});99100const flatData = useMemo(101() => data?.pages.flatMap((page) => page.data) ?? [],102[data],103);104105const totalDBRowCount = data?.pages?.[0]?.meta?.totalRowCount ?? 0;106const totalFetched = flatData.length;107108//called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table109const fetchMoreOnBottomReached = useCallback(110(containerRefElement?: HTMLDivElement | null) => {111if (containerRefElement) {112const { scrollHeight, scrollTop, clientHeight } = containerRefElement;113//once the user has scrolled within 400px of the bottom of the table, fetch more data if we can114if (115scrollHeight - scrollTop - clientHeight < 400 &&116!isFetching &&117totalFetched < totalDBRowCount118) {119fetchNextPage();120}121}122},123[fetchNextPage, isFetching, totalFetched, totalDBRowCount],124);125126//scroll to top of table when sorting or filters change127useEffect(() => {128if (rowVirtualizerInstanceRef.current) {129try {130rowVirtualizerInstanceRef.current.scrollToIndex(0);131} catch (e) {132console.error(e);133}134}135}, [sorting, columnFilters, globalFilter]);136137//a check on mount to see if the table is already scrolled to the bottom and immediately needs to fetch more data138useEffect(() => {139fetchMoreOnBottomReached(tableContainerRef.current);140}, [fetchMoreOnBottomReached]);141142const table = useMantineReactTable({143columns,144data: flatData,145enablePagination: false,146enableRowNumbers: true,147enableRowVirtualization: true, //optional, but recommended if it is likely going to be more than 100 rows148manualFiltering: true,149manualSorting: true,150mantineTableContainerProps: {151ref: tableContainerRef, //get access to the table container element152sx: { maxHeight: '600px' }, //give the table a max height153onScroll: (154event: UIEvent<HTMLDivElement>, //add an event listener to the table container element155) => fetchMoreOnBottomReached(event.target as HTMLDivElement),156},157mantineToolbarAlertBannerProps: {158color: 'red',159children: 'Error loading data',160},161onColumnFiltersChange: setColumnFilters,162onGlobalFilterChange: setGlobalFilter,163onSortingChange: setSorting,164renderBottomToolbarCustomActions: () => (165<Text>166Fetched {totalFetched} of {totalDBRowCount} total rows.167</Text>168),169state: {170columnFilters,171globalFilter,172isLoading,173showAlertBanner: isError,174showProgressBars: isFetching,175sorting,176},177rowVirtualizerInstanceRef, //get access to the virtualizer instance178rowVirtualizerProps: { overscan: 10 },179});180181return <MantineReactTable table={table} />;182};183184const queryClient = new QueryClient();185186const ExampleWithReactQueryProvider = () => (187<QueryClientProvider client={queryClient}>188<Example />189</QueryClientProvider>190);191192export default ExampleWithReactQueryProvider;
1import { useCallback, useEffect, useMemo, useRef, useState } from 'react';2import { MantineReactTable, useMantineReactTable } from 'mantine-react-table';3import { Text } from '@mantine/core';4import {5QueryClient,6QueryClientProvider,7useInfiniteQuery,8} from '@tanstack/react-query';910const columns = [11{12accessorKey: 'firstName',13header: 'First Name',14},15{16accessorKey: 'lastName',17header: 'Last Name',18},19{20accessorKey: 'address',21header: 'Address',22},23{24accessorKey: 'state',25header: 'State',26},27{28accessorKey: 'phoneNumber',29header: 'Phone Number',30},31];3233const fetchSize = 25;3435const Example = () => {36const tableContainerRef = useRef(null); //we can get access to the underlying TableContainer element and react to its scroll events37const rowVirtualizerInstanceRef = useRef(null); //we can get access to the underlying Virtualizer instance and call its scrollToIndex method3839const [columnFilters, setColumnFilters] = useState([]);40const [globalFilter, setGlobalFilter] = useState();41const [sorting, setSorting] = useState([]);4243const { data, fetchNextPage, isError, isFetching, isLoading } =44useInfiniteQuery({45queryKey: ['table-data', columnFilters, globalFilter, sorting],46queryFn: async ({ pageParam = 0 }) => {47const url = new URL(48'/api/data',49process.env.NODE_ENV === 'production'50? 'https://www.mantine-react-table.com'51: 'http://localhost:3001',52);53url.searchParams.set('start', `${pageParam * fetchSize}`);54url.searchParams.set('size', `${fetchSize}`);55url.searchParams.set('filters', JSON.stringify(columnFilters ?? []));56url.searchParams.set('globalFilter', globalFilter ?? '');57url.searchParams.set('sorting', JSON.stringify(sorting ?? []));5859const response = await fetch(url.href);60const json = await response.json();61return json;62},63getNextPageParam: (_lastGroup, groups) => groups.length,64keepPreviousData: true,65refetchOnWindowFocus: false,66});6768const flatData = useMemo(69() => data?.pages.flatMap((page) => page.data) ?? [],70[data],71);7273const totalDBRowCount = data?.pages?.[0]?.meta?.totalRowCount ?? 0;74const totalFetched = flatData.length;7576//called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table77const fetchMoreOnBottomReached = useCallback(78(containerRefElement) => {79if (containerRefElement) {80const { scrollHeight, scrollTop, clientHeight } = containerRefElement;81//once the user has scrolled within 400px of the bottom of the table, fetch more data if we can82if (83scrollHeight - scrollTop - clientHeight < 400 &&84!isFetching &&85totalFetched < totalDBRowCount86) {87fetchNextPage();88}89}90},91[fetchNextPage, isFetching, totalFetched, totalDBRowCount],92);9394//scroll to top of table when sorting or filters change95useEffect(() => {96if (rowVirtualizerInstanceRef.current) {97try {98rowVirtualizerInstanceRef.current.scrollToIndex(0);99} catch (e) {100console.error(e);101}102}103}, [sorting, columnFilters, globalFilter]);104105//a check on mount to see if the table is already scrolled to the bottom and immediately needs to fetch more data106useEffect(() => {107fetchMoreOnBottomReached(tableContainerRef.current);108}, [fetchMoreOnBottomReached]);109110const table = useMantineReactTable({111columns,112data: flatData,113enablePagination: false,114enableRowNumbers: true,115enableRowVirtualization: true, //optional, but recommended if it is likely going to be more than 100 rows116manualFiltering: true,117manualSorting: true,118mantineTableContainerProps: {119ref: tableContainerRef, //get access to the table container element120sx: { maxHeight: '600px' }, //give the table a max height121onScroll: (122event, //add an event listener to the table container element123) => fetchMoreOnBottomReached(event.target),124},125mantineToolbarAlertBannerProps: {126color: 'red',127children: 'Error loading data',128},129onColumnFiltersChange: setColumnFilters,130onGlobalFilterChange: setGlobalFilter,131onSortingChange: setSorting,132renderBottomToolbarCustomActions: () => (133<Text>134Fetched {totalFetched} of {totalDBRowCount} total rows.135</Text>136),137state: {138columnFilters,139globalFilter,140isLoading,141showAlertBanner: isError,142showProgressBars: isFetching,143sorting,144},145rowVirtualizerInstanceRef, //get access to the virtualizer instance146rowVirtualizerProps: { overscan: 10 },147});148149return <MantineReactTable table={table} />;150};151152const queryClient = new QueryClient();153154const ExampleWithReactQueryProvider = () => (155<QueryClientProvider client={queryClient}>156<Example />157</QueryClientProvider>158);159160export 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}
View Extra Storybook Examples