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 Material 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 |
---|---|---|---|---|---|
1import React, {2 FC,3 UIEvent,4 useCallback,5 useEffect,6 useMemo,7 useRef,8 useState,9} from 'react';10import MaterialReactTable, {11 MRT_ColumnDef,12 Virtualizer,13} from 'material-react-table';14import { Typography } from '@mui/material';15import type { ColumnFiltersState, SortingState } from '@tanstack/react-table';16import {17 QueryClient,18 QueryClientProvider,19 useInfiniteQuery,20} from '@tanstack/react-query';21import axios from 'axios';2223type UserApiResponse = {24 data: Array<User>;25 meta: {26 totalRowCount: number;27 };28};2930type User = {31 firstName: string;32 lastName: string;33 address: string;34 state: string;35 phoneNumber: string;36};3738const columns: MRT_ColumnDef<User>[] = [39 {40 accessorKey: 'firstName',41 header: 'First Name',42 },43 {44 accessorKey: 'lastName',45 header: 'Last Name',46 },47 {48 accessorKey: 'address',49 header: 'Address',50 },51 {52 accessorKey: 'state',53 header: 'State',54 },55 {56 accessorKey: 'phoneNumber',57 header: 'Phone Number',58 },59];6061const fetchSize = 25;6263const Example: FC = () => {64 const tableContainerRef = useRef<HTMLDivElement>(null); //we can get access to the underlying TableContainer element and react to its scroll events65 const virtualizerInstanceRef = useRef<Virtualizer>(null); //we can get access to the underlying Virtualizer instance and call its scrollToIndex method6667 const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);68 const [globalFilter, setGlobalFilter] = useState<string>();69 const [sorting, setSorting] = useState<SortingState>([]);7071 const { data, fetchNextPage, isError, isFetching, isLoading } =72 useInfiniteQuery<UserApiResponse>(73 ['table-data', columnFilters, globalFilter, sorting],74 async ({ pageParam = 0 }) => {75 const url = new URL(76 '/api/data',77 'https://www.material-react-table.com',78 );79 url.searchParams.set('start', `${pageParam * fetchSize}`);80 url.searchParams.set('size', `${fetchSize}`);81 url.searchParams.set('filters', JSON.stringify(columnFilters ?? []));82 url.searchParams.set('globalFilter', globalFilter ?? '');83 url.searchParams.set('sorting', JSON.stringify(sorting ?? []));8485 const { data: axiosData } = await axios.get(url.href);86 return axiosData;87 },88 {89 getNextPageParam: (_lastGroup, groups) => groups.length,90 keepPreviousData: true,91 refetchOnWindowFocus: false,92 },93 );9495 const flatData = useMemo(96 () => data?.pages.flatMap((page) => page.data) ?? [],97 [data],98 );99100 const totalDBRowCount = data?.pages?.[0]?.meta?.totalRowCount ?? 0;101 const totalFetched = flatData.length;102103 //called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table104 const fetchMoreOnBottomReached = useCallback(105 (containerRefElement?: HTMLDivElement | null) => {106 if (containerRefElement) {107 const { scrollHeight, scrollTop, clientHeight } = containerRefElement;108 //once the user has scrolled within 200px of the bottom of the table, fetch more data if we can109 if (110 scrollHeight - scrollTop - clientHeight < 200 &&111 !isFetching &&112 totalFetched < totalDBRowCount113 ) {114 fetchNextPage();115 }116 }117 },118 [fetchNextPage, isFetching, totalFetched, totalDBRowCount],119 );120121 //scroll to top of table when sorting or filters change122 useEffect(() => {123 if (virtualizerInstanceRef.current) {124 virtualizerInstanceRef.current.scrollToIndex(0);125 }126 }, [sorting, columnFilters, globalFilter]);127128 //a check on mount to see if the table is already scrolled to the bottom and immediately needs to fetch more data129 useEffect(() => {130 fetchMoreOnBottomReached(tableContainerRef.current);131 }, [fetchMoreOnBottomReached]);132133 return (134 <MaterialReactTable135 columns={columns}136 data={flatData}137 enablePagination={false}138 enableRowNumbers139 enableRowVirtualization //optional, but recommended if it is likely going to be more than 100 rows140 manualFiltering141 manualSorting142 muiTableContainerProps={{143 ref: tableContainerRef, //get access to the table container element144 sx: { maxHeight: '600px' }, //give the table a max height145 onScroll: (146 event: UIEvent<HTMLDivElement>, //add an event listener to the table container element147 ) => fetchMoreOnBottomReached(event.target as HTMLDivElement),148 }}149 muiToolbarAlertBannerProps={150 isError151 ? {152 color: 'error',153 children: 'Error loading data',154 }155 : undefined156 }157 onColumnFiltersChange={setColumnFilters}158 onGlobalFilterChange={setGlobalFilter}159 onSortingChange={setSorting}160 renderBottomToolbarCustomActions={() => (161 <Typography>162 Fetched {totalFetched} of {totalDBRowCount} total rows.163 </Typography>164 )}165 state={{166 columnFilters,167 globalFilter,168 isLoading,169 showAlertBanner: isError,170 showProgressBars: isFetching,171 sorting,172 }}173 virtualizerInstanceRef={virtualizerInstanceRef} //get access to the virtualizer instance174 />175 );176};177178const queryClient = new QueryClient();179180const ExampleWithReactQueryProvider = () => (181 <QueryClientProvider client={queryClient}>182 <Example />183 </QueryClientProvider>184);185186export default ExampleWithReactQueryProvider;187
View Extra Storybook Examples