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 |
---|
Fetched 0 of 0 total rows.
1import {2 type UIEvent,3 useCallback,4 useEffect,5 useMemo,6 useRef,7 useState,8} from 'react';9import {10 MaterialReactTable,11 type MRT_ColumnDef,12 type MRT_ColumnFiltersState,13 type MRT_SortingState,14 type MRT_Virtualizer,15} from 'material-react-table';16import { Typography } from '@mui/material';17import {18 QueryClient,19 QueryClientProvider,20 useInfiniteQuery,21} from '@tanstack/react-query';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 = () => {64 const tableContainerRef = useRef<HTMLDivElement>(null); //we can get access to the underlying TableContainer element and react to its scroll events65 const rowVirtualizerInstanceRef =66 useRef<MRT_Virtualizer<HTMLDivElement, HTMLTableRowElement>>(null); //we can get access to the underlying Virtualizer instance and call its scrollToIndex method6768 const [columnFilters, setColumnFilters] = useState<MRT_ColumnFiltersState>(69 [],70 );71 const [globalFilter, setGlobalFilter] = useState<string>();72 const [sorting, setSorting] = useState<MRT_SortingState>([]);7374 const { data, fetchNextPage, isError, isFetching, isLoading } =75 useInfiniteQuery<UserApiResponse>({76 queryKey: ['table-data', columnFilters, globalFilter, sorting],77 queryFn: async ({ pageParam }) => {78 const url = new URL(79 '/api/data',80 process.env.NODE_ENV === 'production'81 ? 'https://www.material-react-table.com'82 : 'http://localhost:3000',83 );84 url.searchParams.set('start', `${(pageParam as number) * fetchSize}`);85 url.searchParams.set('size', `${fetchSize}`);86 url.searchParams.set('filters', JSON.stringify(columnFilters ?? []));87 url.searchParams.set('globalFilter', globalFilter ?? '');88 url.searchParams.set('sorting', JSON.stringify(sorting ?? []));8990 const response = await fetch(url.href);91 const json = (await response.json()) as UserApiResponse;92 return json;93 },94 initialPageParam: 0,95 getNextPageParam: (_lastGroup, groups) => groups.length,96 refetchOnWindowFocus: false,97 });9899 const flatData = useMemo(100 () => data?.pages.flatMap((page) => page.data) ?? [],101 [data],102 );103104 const totalDBRowCount = data?.pages?.[0]?.meta?.totalRowCount ?? 0;105 const totalFetched = flatData.length;106107 //called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table108 const fetchMoreOnBottomReached = useCallback(109 (containerRefElement?: HTMLDivElement | null) => {110 if (containerRefElement) {111 const { scrollHeight, scrollTop, clientHeight } = containerRefElement;112 //once the user has scrolled within 400px of the bottom of the table, fetch more data if we can113 if (114 scrollHeight - scrollTop - clientHeight < 400 &&115 !isFetching &&116 totalFetched < totalDBRowCount117 ) {118 fetchNextPage();119 }120 }121 },122 [fetchNextPage, isFetching, totalFetched, totalDBRowCount],123 );124125 //scroll to top of table when sorting or filters change126 useEffect(() => {127 //scroll to the top of the table when the sorting changes128 try {129 rowVirtualizerInstanceRef.current?.scrollToIndex?.(0);130 } catch (error) {131 console.error(error);132 }133 }, [sorting, columnFilters, globalFilter]);134135 //a check on mount to see if the table is already scrolled to the bottom and immediately needs to fetch more data136 useEffect(() => {137 fetchMoreOnBottomReached(tableContainerRef.current);138 }, [fetchMoreOnBottomReached]);139140 return (141 <MaterialReactTable142 columns={columns}143 data={flatData}144 enablePagination={false}145 enableRowNumbers146 enableRowVirtualization //optional, but recommended if it is likely going to be more than 100 rows147 manualFiltering148 manualSorting149 muiTableContainerProps={{150 ref: tableContainerRef, //get access to the table container element151 sx: { maxHeight: '600px' }, //give the table a max height152 onScroll: (153 event: UIEvent<HTMLDivElement>, //add an event listener to the table container element154 ) => fetchMoreOnBottomReached(event.target as HTMLDivElement),155 }}156 muiToolbarAlertBannerProps={157 isError158 ? {159 color: 'error',160 children: 'Error loading data',161 }162 : undefined163 }164 onColumnFiltersChange={setColumnFilters}165 onGlobalFilterChange={setGlobalFilter}166 onSortingChange={setSorting}167 renderBottomToolbarCustomActions={() => (168 <Typography>169 Fetched {totalFetched} of {totalDBRowCount} total rows.170 </Typography>171 )}172 state={{173 columnFilters,174 globalFilter,175 isLoading,176 showAlertBanner: isError,177 showProgressBars: isFetching,178 sorting,179 }}180 rowVirtualizerInstanceRef={rowVirtualizerInstanceRef} //get access to the virtualizer instance181 rowVirtualizerOptions={{ overscan: 4 }}182 />183 );184};185186const queryClient = new QueryClient();187188const ExampleWithReactQueryProvider = () => (189 <QueryClientProvider client={queryClient}>190 <Example />191 </QueryClientProvider>192);193194export default ExampleWithReactQueryProvider;195
View Extra Storybook Examples