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 useCallback,3 useEffect,4 useMemo,5 useRef,6 useState,7} from 'react';8import MaterialReactTable from 'material-react-table';9import { Typography } from '@mui/material';10import {11 QueryClient,12 QueryClientProvider,13 useInfiniteQuery,14} from '@tanstack/react-query';1516const columns = [17 {18 accessorKey: 'firstName',19 header: 'First Name',20 },21 {22 accessorKey: 'lastName',23 header: 'Last Name',24 },25 {26 accessorKey: 'address',27 header: 'Address',28 },29 {30 accessorKey: 'state',31 header: 'State',32 },33 {34 accessorKey: 'phoneNumber',35 header: 'Phone Number',36 },37];3839const fetchSize = 25;4041const Example = () => {42 const tableContainerRef = useRef(null); //we can get access to the underlying TableContainer element and react to its scroll events43 const virtualizerInstanceRef = useRef < Virtualizer > null; //we can get access to the underlying Virtualizer instance and call its scrollToIndex method4445 const [columnFilters, setColumnFilters] = useState([]);46 const [globalFilter, setGlobalFilter] = useState();47 const [sorting, setSorting] = useState([]);4849 const { data, fetchNextPage, isError, isFetching, isLoading } =50 useInfiniteQuery(51 ['table-data', columnFilters, globalFilter, sorting],52 async ({ pageParam = 0 }) => {53 const url = new URL(54 '/api/data',55 process.env.NODE_ENV === 'production'56 ? 'https://www.material-react-table.com'57 : 'http://localhost:3000',58 );59 url.searchParams.set('start', `${pageParam * fetchSize}`);60 url.searchParams.set('size', `${fetchSize}`);61 url.searchParams.set('filters', JSON.stringify(columnFilters ?? []));62 url.searchParams.set('globalFilter', globalFilter ?? '');63 url.searchParams.set('sorting', JSON.stringify(sorting ?? []));6465 const response = await fetch(url.href);66 const json = await response.json();67 return json;68 },69 {70 getNextPageParam: (_lastGroup, groups) => groups.length,71 keepPreviousData: true,72 refetchOnWindowFocus: false,73 },74 );7576 const flatData = useMemo(77 () => data?.pages.flatMap((page) => page.data) ?? [],78 [data],79 );8081 const totalDBRowCount = data?.pages?.[0]?.meta?.totalRowCount ?? 0;82 const totalFetched = flatData.length;8384 //called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table85 const fetchMoreOnBottomReached = useCallback(86 (containerRefElement) => {87 if (containerRefElement) {88 const { scrollHeight, scrollTop, clientHeight } = containerRefElement;89 //once the user has scrolled within 200px of the bottom of the table, fetch more data if we can90 if (91 scrollHeight - scrollTop - clientHeight < 200 &&92 !isFetching &&93 totalFetched < totalDBRowCount94 ) {95 fetchNextPage();96 }97 }98 },99 [fetchNextPage, isFetching, totalFetched, totalDBRowCount],100 );101102 //scroll to top of table when sorting or filters change103 useEffect(() => {104 if (virtualizerInstanceRef.current) {105 virtualizerInstanceRef.current.scrollToIndex(0);106 }107 }, [sorting, columnFilters, globalFilter]);108109 //a check on mount to see if the table is already scrolled to the bottom and immediately needs to fetch more data110 useEffect(() => {111 fetchMoreOnBottomReached(tableContainerRef.current);112 }, [fetchMoreOnBottomReached]);113114 return (115 <MaterialReactTable116 columns={columns}117 data={flatData}118 enablePagination={false}119 enableRowNumbers120 enableRowVirtualization //optional, but recommended if it is likely going to be more than 100 rows121 manualFiltering122 manualSorting123 muiTableContainerProps={{124 ref: tableContainerRef, //get access to the table container element125 sx: { maxHeight: '600px' }, //give the table a max height126 onScroll: (127 event, //add an event listener to the table container element128 ) => fetchMoreOnBottomReached(event.target),129 }}130 muiToolbarAlertBannerProps={131 isError132 ? {133 color: 'error',134 children: 'Error loading data',135 }136 : undefined137 }138 onColumnFiltersChange={setColumnFilters}139 onGlobalFilterChange={setGlobalFilter}140 onSortingChange={setSorting}141 renderBottomToolbarCustomActions={() => (142 <Typography>143 Fetched {totalFetched} of {totalDBRowCount} total rows.144 </Typography>145 )}146 state={{147 columnFilters,148 globalFilter,149 isLoading,150 showAlertBanner: isError,151 showProgressBars: isFetching,152 sorting,153 }}154 virtualizerInstanceRef={virtualizerInstanceRef} //get access to the virtualizer instance155 />156 );157};158159const queryClient = new QueryClient();160161const ExampleWithReactQueryProvider = () => (162 <QueryClientProvider client={queryClient}>163 <Example />164 </QueryClientProvider>165);166167export default ExampleWithReactQueryProvider;168
View Extra Storybook Examples