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 useMaterialReactTable,12 type MRT_ColumnDef,13 type MRT_ColumnFiltersState,14 type MRT_SortingState,15 type MRT_RowVirtualizer,16} from 'material-react-table';17import { Typography } from '@mui/material';18import {19 QueryClient,20 QueryClientProvider,21 useInfiniteQuery,22} from '@tanstack/react-query'; //Note: this is TanStack React Query V52324//Your API response shape will probably be different. Knowing a total row count is important though.25type UserApiResponse = {26 data: Array<User>;27 meta: {28 totalRowCount: number;29 };30};3132type User = {33 firstName: string;34 lastName: string;35 address: string;36 state: string;37 phoneNumber: string;38};3940const columns: MRT_ColumnDef<User>[] = [41 {42 accessorKey: 'firstName',43 header: 'First Name',44 },45 {46 accessorKey: 'lastName',47 header: 'Last Name',48 },49 {50 accessorKey: 'address',51 header: 'Address',52 },53 {54 accessorKey: 'state',55 header: 'State',56 },57 {58 accessorKey: 'phoneNumber',59 header: 'Phone Number',60 },61];6263const fetchSize = 25;6465const Example = () => {66 const tableContainerRef = useRef<HTMLDivElement>(null); //we can get access to the underlying TableContainer element and react to its scroll events67 const rowVirtualizerInstanceRef = useRef<MRT_RowVirtualizer>(null); //we can get access to the underlying Virtualizer instance and call its scrollToIndex method68 const [currentMaxPages, setCurrentMaxPages] = useState(4);69 const [columnFilters, setColumnFilters] = useState<MRT_ColumnFiltersState>(70 [],71 );72 const [globalFilter, setGlobalFilter] = useState<string>();73 const [sorting, setSorting] = useState<MRT_SortingState>([]);7475 const { data, fetchNextPage, isError, isFetching, isLoading } =76 useInfiniteQuery<UserApiResponse>({77 queryKey: [78 'table-data',79 columnFilters, //refetch when columnFilters changes80 globalFilter, //refetch when globalFilter changes81 sorting, //refetch when sorting changes82 ],83 queryFn: async ({ pageParam }) => {84 const url = new URL(85 '/api/data',86 process.env.NODE_ENV === 'production'87 ? 'https://www.material-react-table.com'88 : 'http://localhost:3000',89 );90 url.searchParams.set('start', `${(pageParam as number) * fetchSize}`);91 url.searchParams.set('size', `${fetchSize}`);92 url.searchParams.set('filters', JSON.stringify(columnFilters ?? []));93 url.searchParams.set('globalFilter', globalFilter ?? '');94 url.searchParams.set('sorting', JSON.stringify(sorting ?? []));9596 const response = await fetch(url.href);97 const json = (await response.json()) as UserApiResponse;98 return json;99 },100 initialPageParam: 0,101 getNextPageParam: (_lastGroup, groups) => groups.length,102 refetchOnWindowFocus: false,103 maxPages: currentMaxPages,104 });105106 const flatData = useMemo(107 () => data?.pages.flatMap((page) => page.data) ?? [],108 [data],109 );110111 const totalDBRowCount = data?.pages?.[0]?.meta?.totalRowCount ?? 0;112 const totalFetched = flatData.length;113114 const handleScroll = useCallback(() => {115 const containerElement = tableContainerRef.current;116 if (containerElement) {117 const { scrollHeight, scrollTop, clientHeight } = containerElement;118119 if (120 scrollHeight - scrollTop - clientHeight < 400 &&121 !isFetching &&122 totalFetched < totalDBRowCount &&123 currentMaxPages < totalDBRowCount / fetchSize124 ) {125 setCurrentMaxPages((prevMaxPages) => prevMaxPages + 2);126 }127 }128 }, [currentMaxPages, isFetching, totalFetched, totalDBRowCount]);129130 //called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table131 const fetchMoreOnBottomReached = useCallback(132 (containerRefElement?: HTMLDivElement | null) => {133 if (containerRefElement) {134 const { scrollHeight, scrollTop, clientHeight } = containerRefElement;135 //once the user has scrolled within 400px of the bottom of the table, fetch more data if we can136 if (137 scrollHeight - scrollTop - clientHeight < 400 &&138 !isFetching &&139 totalFetched < totalDBRowCount140 ) {141 fetchNextPage();142 }143 }144 },145 [fetchNextPage, isFetching, totalFetched, totalDBRowCount],146 );147148 //scroll to top of table when sorting or filters change149 useEffect(() => {150 //scroll to the top of the table when the sorting changes151 try {152 rowVirtualizerInstanceRef.current?.scrollToIndex?.(0);153 } catch (error) {154 console.error(error);155 }156 }, [sorting, columnFilters, globalFilter]);157158 useEffect(() => {159 const containerElement = tableContainerRef.current;160 if (containerElement) {161 containerElement.addEventListener('scroll', handleScroll);162 return () => {163 containerElement.removeEventListener('scroll', handleScroll);164 };165 }166 }, [handleScroll]);167168 //a check on mount to see if the table is already scrolled to the bottom and immediately needs to fetch more data169 useEffect(() => {170 fetchMoreOnBottomReached(tableContainerRef.current);171 }, [fetchMoreOnBottomReached]);172173 const table = useMaterialReactTable({174 columns,175 data: flatData,176 enablePagination: false,177 enableRowNumbers: true,178 enableRowVirtualization: true,179 manualFiltering: true,180 manualSorting: true,181 muiTableContainerProps: {182 ref: tableContainerRef, //get access to the table container element183 sx: { maxHeight: '600px' }, //give the table a max height184 onScroll: (event: UIEvent<HTMLDivElement>) =>185 fetchMoreOnBottomReached(event.target as HTMLDivElement), //add an event listener to the table container element186 },187 muiToolbarAlertBannerProps: isError188 ? {189 color: 'error',190 children: 'Error loading data',191 }192 : undefined,193 onColumnFiltersChange: setColumnFilters,194 onGlobalFilterChange: setGlobalFilter,195 onSortingChange: setSorting,196 renderBottomToolbarCustomActions: () => (197 <Typography>198 Fetched {totalFetched} of {totalDBRowCount} total rows.199 </Typography>200 ),201 state: {202 columnFilters,203 globalFilter,204 isLoading,205 showAlertBanner: isError,206 showProgressBars: isFetching,207 sorting,208 },209 rowVirtualizerInstanceRef, //get access to the virtualizer instance210 rowVirtualizerOptions: { overscan: 4 },211 });212213 return <MaterialReactTable table={table} />;214};215216const queryClient = new QueryClient();217218const ExampleWithReactQueryProvider = () => (219 //App.tsx or AppProviders file. Don't just wrap this component with QueryClientProvider! Wrap your whole App!220 <QueryClientProvider client={queryClient}>221 <Example />222 </QueryClientProvider>223);224225export default ExampleWithReactQueryProvider;226
View Extra Storybook Examples