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 lazy,3 Suspense,4 type UIEvent,5 useCallback,6 useEffect,7 useMemo,8 useRef,9 useState,10} from 'react';11import {12 MaterialReactTable,13 useMaterialReactTable,14 type MRT_ColumnDef,15 type MRT_ColumnFiltersState,16 type MRT_SortingState,17 type MRT_RowVirtualizer,18} from 'material-react-table';19import { Typography } from '@mui/material';20import {21 QueryClient,22 QueryClientProvider,23 useInfiniteQuery,24} from '@tanstack/react-query'; //Note: this is TanStack React Query V52526//Your API response shape will probably be different. Knowing a total row count is important though.27type UserApiResponse = {28 data: Array<User>;29 meta: {30 totalRowCount: number;31 };32};3334type User = {35 firstName: string;36 lastName: string;37 address: string;38 state: string;39 phoneNumber: string;40};4142const columns: MRT_ColumnDef<User>[] = [43 {44 accessorKey: 'firstName',45 header: 'First Name',46 },47 {48 accessorKey: 'lastName',49 header: 'Last Name',50 },51 {52 accessorKey: 'address',53 header: 'Address',54 },55 {56 accessorKey: 'state',57 header: 'State',58 },59 {60 accessorKey: 'phoneNumber',61 header: 'Phone Number',62 },63];6465const fetchSize = 25;6667const Example = () => {68 const tableContainerRef = useRef<HTMLDivElement>(null); //we can get access to the underlying TableContainer element and react to its scroll events69 const rowVirtualizerInstanceRef = useRef<MRT_RowVirtualizer>(null); //we can get access to the underlying Virtualizer instance and call its scrollToIndex method7071 const [columnFilters, setColumnFilters] = useState<MRT_ColumnFiltersState>(72 [],73 );74 const [globalFilter, setGlobalFilter] = useState<string>();75 const [sorting, setSorting] = useState<MRT_SortingState>([]);7677 const { data, fetchNextPage, isError, isFetching, isLoading } =78 useInfiniteQuery<UserApiResponse>({79 queryKey: [80 'users-list',81 {82 columnFilters, //refetch when columnFilters changes83 globalFilter, //refetch when globalFilter changes84 sorting, //refetch when sorting changes85 },86 ],87 queryFn: async ({ pageParam }) => {88 const url = new URL('/api/data', location.origin); // nextjs api route89 url.searchParams.set('start', `${(pageParam as number) * fetchSize}`);90 url.searchParams.set('size', `${fetchSize}`);91 url.searchParams.set('filters', JSON.stringify(columnFilters ?? []));92 url.searchParams.set('globalFilter', globalFilter ?? '');93 url.searchParams.set('sorting', JSON.stringify(sorting ?? []));9495 const response = await fetch(url.href);96 const json = (await response.json()) as UserApiResponse;97 return json;98 },99 initialPageParam: 0,100 getNextPageParam: (_lastGroup, groups) => groups.length,101 refetchOnWindowFocus: false,102 });103104 const flatData = useMemo(105 () => data?.pages.flatMap((page) => page.data) ?? [],106 [data],107 );108109 const totalDBRowCount = data?.pages?.[0]?.meta?.totalRowCount ?? 0;110 const totalFetched = flatData.length;111112 //called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table113 const fetchMoreOnBottomReached = useCallback(114 (containerRefElement?: HTMLDivElement | null) => {115 if (containerRefElement) {116 const { scrollHeight, scrollTop, clientHeight } = containerRefElement;117 //once the user has scrolled within 400px of the bottom of the table, fetch more data if we can118 if (119 scrollHeight - scrollTop - clientHeight < 400 &&120 !isFetching &&121 totalFetched < totalDBRowCount122 ) {123 fetchNextPage();124 }125 }126 },127 [fetchNextPage, isFetching, totalFetched, totalDBRowCount],128 );129130 //scroll to top of table when sorting or filters change131 useEffect(() => {132 //scroll to the top of the table when the sorting changes133 try {134 rowVirtualizerInstanceRef.current?.scrollToIndex?.(0);135 } catch (error) {136 console.error(error);137 }138 }, [sorting, columnFilters, globalFilter]);139140 //a check on mount to see if the table is already scrolled to the bottom and immediately needs to fetch more data141 useEffect(() => {142 fetchMoreOnBottomReached(tableContainerRef.current);143 }, [fetchMoreOnBottomReached]);144145 const table = useMaterialReactTable({146 columns,147 data: flatData,148 enablePagination: false,149 enableRowNumbers: true,150 enableRowVirtualization: true,151 manualFiltering: true,152 manualSorting: true,153 muiTableContainerProps: {154 ref: tableContainerRef, //get access to the table container element155 sx: { maxHeight: '600px' }, //give the table a max height156 onScroll: (event: UIEvent<HTMLDivElement>) =>157 fetchMoreOnBottomReached(event.target as HTMLDivElement), //add an event listener to the table container element158 },159 muiToolbarAlertBannerProps: isError160 ? {161 color: 'error',162 children: 'Error loading data',163 }164 : undefined,165 onColumnFiltersChange: setColumnFilters,166 onGlobalFilterChange: setGlobalFilter,167 onSortingChange: setSorting,168 renderBottomToolbarCustomActions: () => (169 <Typography>170 Fetched {totalFetched} of {totalDBRowCount} total rows.171 </Typography>172 ),173 state: {174 columnFilters,175 globalFilter,176 isLoading,177 showAlertBanner: isError,178 showProgressBars: isFetching,179 sorting,180 },181 rowVirtualizerInstanceRef, //get access to the virtualizer instance182 rowVirtualizerOptions: { overscan: 4 },183 });184185 return <MaterialReactTable table={table} />;186};187188//react query setup in App.tsx189const ReactQueryDevtoolsProduction = lazy(() =>190 import('@tanstack/react-query-devtools/build/modern/production.js').then(191 (d) => ({192 default: d.ReactQueryDevtools,193 }),194 ),195);196197const queryClient = new QueryClient();198199export default function App() {200 return (201 <QueryClientProvider client={queryClient}>202 <Example />203 <Suspense fallback={null}>204 <ReactQueryDevtoolsProduction />205 </Suspense>206 </QueryClientProvider>207 );208}209
View Extra Storybook Examples