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, { MRT_ColumnDef } from 'material-react-table';11import { Typography } from '@mui/material';12import type { ColumnFiltersState, SortingState } from '@tanstack/react-table';13import type { Virtualizer } from '@tanstack/react-virtual';14import {15 QueryClient,16 QueryClientProvider,17 useInfiniteQuery,18} from '@tanstack/react-query';1920type UserApiResponse = {21 data: Array<User>;22 meta: {23 totalRowCount: number;24 };25};2627type User = {28 firstName: string;29 lastName: string;30 address: string;31 state: string;32 phoneNumber: string;33};3435const columns: MRT_ColumnDef<User>[] = [36 {37 accessorKey: 'firstName',38 header: 'First Name',39 },40 {41 accessorKey: 'lastName',42 header: 'Last Name',43 },44 {45 accessorKey: 'address',46 header: 'Address',47 },48 {49 accessorKey: 'state',50 header: 'State',51 },52 {53 accessorKey: 'phoneNumber',54 header: 'Phone Number',55 },56];5758const fetchSize = 25;5960const Example: FC = () => {61 const tableContainerRef = useRef<HTMLDivElement>(null); //we can get access to the underlying TableContainer element and react to its scroll events62 const virtualizerInstanceRef =63 useRef<Virtualizer<HTMLDivElement, HTMLTableRowElement>>(null); //we can get access to the underlying Virtualizer instance and call its scrollToIndex method6465 const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);66 const [globalFilter, setGlobalFilter] = useState<string>();67 const [sorting, setSorting] = useState<SortingState>([]);6869 const { data, fetchNextPage, isError, isFetching, isLoading } =70 useInfiniteQuery<UserApiResponse>(71 ['table-data', columnFilters, globalFilter, sorting],72 async ({ pageParam = 0 }) => {73 const url = new URL(74 '/api/data',75 process.env.NODE_ENV === 'production'76 ? 'https://www.material-react-table.com'77 : 'http://localhost:3000',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 response = await fetch(url.href);86 const json = (await response.json()) as UserApiResponse;87 return json;88 },89 {90 getNextPageParam: (_lastGroup, groups) => groups.length,91 keepPreviousData: true,92 refetchOnWindowFocus: false,93 },94 );9596 const flatData = useMemo(97 () => data?.pages.flatMap((page) => page.data) ?? [],98 [data],99 );100101 const totalDBRowCount = data?.pages?.[0]?.meta?.totalRowCount ?? 0;102 const totalFetched = flatData.length;103104 //called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table105 const fetchMoreOnBottomReached = useCallback(106 (containerRefElement?: HTMLDivElement | null) => {107 if (containerRefElement) {108 const { scrollHeight, scrollTop, clientHeight } = containerRefElement;109 //once the user has scrolled within 200px of the bottom of the table, fetch more data if we can110 if (111 scrollHeight - scrollTop - clientHeight < 200 &&112 !isFetching &&113 totalFetched < totalDBRowCount114 ) {115 fetchNextPage();116 }117 }118 },119 [fetchNextPage, isFetching, totalFetched, totalDBRowCount],120 );121122 //scroll to top of table when sorting or filters change123 useEffect(() => {124 if (virtualizerInstanceRef.current) {125 virtualizerInstanceRef.current.scrollToIndex(0);126 }127 }, [sorting, columnFilters, globalFilter]);128129 //a check on mount to see if the table is already scrolled to the bottom and immediately needs to fetch more data130 useEffect(() => {131 fetchMoreOnBottomReached(tableContainerRef.current);132 }, [fetchMoreOnBottomReached]);133134 return (135 <MaterialReactTable136 columns={columns}137 data={flatData}138 enablePagination={false}139 enableRowNumbers140 enableRowVirtualization //optional, but recommended if it is likely going to be more than 100 rows141 manualFiltering142 manualSorting143 muiTableContainerProps={{144 ref: tableContainerRef, //get access to the table container element145 sx: { maxHeight: '600px' }, //give the table a max height146 onScroll: (147 event: UIEvent<HTMLDivElement>, //add an event listener to the table container element148 ) => fetchMoreOnBottomReached(event.target as HTMLDivElement),149 }}150 muiToolbarAlertBannerProps={151 isError152 ? {153 color: 'error',154 children: 'Error loading data',155 }156 : undefined157 }158 onColumnFiltersChange={setColumnFilters}159 onGlobalFilterChange={setGlobalFilter}160 onSortingChange={setSorting}161 renderBottomToolbarCustomActions={() => (162 <Typography>163 Fetched {totalFetched} of {totalDBRowCount} total rows.164 </Typography>165 )}166 state={{167 columnFilters,168 globalFilter,169 isLoading,170 showAlertBanner: isError,171 showProgressBars: isFetching,172 sorting,173 }}174 virtualizerInstanceRef={virtualizerInstanceRef} //get access to the virtualizer instance175 />176 );177};178179const queryClient = new QueryClient();180181const ExampleWithReactQueryProvider = () => (182 <QueryClientProvider client={queryClient}>183 <Example />184 </QueryClientProvider>185);186187export default ExampleWithReactQueryProvider;188
View Extra Storybook Examples