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 process.env.NODE_ENV === 'production'78 ? 'https://www.material-react-table.com'79 : 'http://localhost:3000',80 );81 url.searchParams.set('start', `${pageParam * fetchSize}`);82 url.searchParams.set('size', `${fetchSize}`);83 url.searchParams.set('filters', JSON.stringify(columnFilters ?? []));84 url.searchParams.set('globalFilter', globalFilter ?? '');85 url.searchParams.set('sorting', JSON.stringify(sorting ?? []));8687 const { data: axiosData } = await axios.get(url.href);88 return axiosData;89 },90 {91 getNextPageParam: (_lastGroup, groups) => groups.length,92 keepPreviousData: true,93 refetchOnWindowFocus: false,94 },95 );9697 const flatData = useMemo(98 () => data?.pages.flatMap((page) => page.data) ?? [],99 [data],100 );101102 const totalDBRowCount = data?.pages?.[0]?.meta?.totalRowCount ?? 0;103 const totalFetched = flatData.length;104105 //called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table106 const fetchMoreOnBottomReached = useCallback(107 (containerRefElement?: HTMLDivElement | null) => {108 if (containerRefElement) {109 const { scrollHeight, scrollTop, clientHeight } = containerRefElement;110 //once the user has scrolled within 200px of the bottom of the table, fetch more data if we can111 if (112 scrollHeight - scrollTop - clientHeight < 200 &&113 !isFetching &&114 totalFetched < totalDBRowCount115 ) {116 fetchNextPage();117 }118 }119 },120 [fetchNextPage, isFetching, totalFetched, totalDBRowCount],121 );122123 //scroll to top of table when sorting or filters change124 useEffect(() => {125 if (virtualizerInstanceRef.current) {126 virtualizerInstanceRef.current.scrollToIndex(0);127 }128 }, [sorting, columnFilters, globalFilter]);129130 //a check on mount to see if the table is already scrolled to the bottom and immediately needs to fetch more data131 useEffect(() => {132 fetchMoreOnBottomReached(tableContainerRef.current);133 }, [fetchMoreOnBottomReached]);134135 return (136 <MaterialReactTable137 columns={columns}138 data={flatData}139 enablePagination={false}140 enableRowNumbers141 enableRowVirtualization //optional, but recommended if it is likely going to be more than 100 rows142 manualFiltering143 manualSorting144 muiTableContainerProps={{145 ref: tableContainerRef, //get access to the table container element146 sx: { maxHeight: '600px' }, //give the table a max height147 onScroll: (148 event: UIEvent<HTMLDivElement>, //add an event listener to the table container element149 ) => fetchMoreOnBottomReached(event.target as HTMLDivElement),150 }}151 muiToolbarAlertBannerProps={152 isError153 ? {154 color: 'error',155 children: 'Error loading data',156 }157 : undefined158 }159 onColumnFiltersChange={setColumnFilters}160 onGlobalFilterChange={setGlobalFilter}161 onSortingChange={setSorting}162 renderBottomToolbarCustomActions={() => (163 <Typography>164 Fetched {totalFetched} of {totalDBRowCount} total rows.165 </Typography>166 )}167 state={{168 columnFilters,169 globalFilter,170 isLoading,171 showAlertBanner: isError,172 showProgressBars: isFetching,173 sorting,174 }}175 virtualizerInstanceRef={virtualizerInstanceRef} //get access to the virtualizer instance176 />177 );178};179180const queryClient = new QueryClient();181182const ExampleWithReactQueryProvider = () => (183 <QueryClientProvider client={queryClient}>184 <Example />185 </QueryClientProvider>186);187188export default ExampleWithReactQueryProvider;189
View Extra Storybook Examples