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 method6869 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 });104105 const flatData = useMemo(106 () => data?.pages.flatMap((page) => page.data) ?? [],107 [data],108 );109110 const totalDBRowCount = data?.pages?.[0]?.meta?.totalRowCount ?? 0;111 const totalFetched = flatData.length;112113 //called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table114 const fetchMoreOnBottomReached = useCallback(115 (containerRefElement?: HTMLDivElement | null) => {116 if (containerRefElement) {117 const { scrollHeight, scrollTop, clientHeight } = containerRefElement;118 //once the user has scrolled within 400px of the bottom of the table, fetch more data if we can119 if (120 scrollHeight - scrollTop - clientHeight < 400 &&121 !isFetching &&122 totalFetched < totalDBRowCount123 ) {124 fetchNextPage();125 }126 }127 },128 [fetchNextPage, isFetching, totalFetched, totalDBRowCount],129 );130131 //scroll to top of table when sorting or filters change132 useEffect(() => {133 //scroll to the top of the table when the sorting changes134 try {135 rowVirtualizerInstanceRef.current?.scrollToIndex?.(0);136 } catch (error) {137 console.error(error);138 }139 }, [sorting, columnFilters, globalFilter]);140141 //a check on mount to see if the table is already scrolled to the bottom and immediately needs to fetch more data142 useEffect(() => {143 fetchMoreOnBottomReached(tableContainerRef.current);144 }, [fetchMoreOnBottomReached]);145146 const table = useMaterialReactTable({147 columns,148 data: flatData,149 enablePagination: false,150 enableRowNumbers: true,151 enableRowVirtualization: true,152 manualFiltering: true,153 manualSorting: true,154 muiTableContainerProps: {155 ref: tableContainerRef, //get access to the table container element156 sx: { maxHeight: '600px' }, //give the table a max height157 onScroll: (event: UIEvent<HTMLDivElement>) =>158 fetchMoreOnBottomReached(event.target as HTMLDivElement), //add an event listener to the table container element159 },160 muiToolbarAlertBannerProps: isError161 ? {162 color: 'error',163 children: 'Error loading data',164 }165 : undefined,166 onColumnFiltersChange: setColumnFilters,167 onGlobalFilterChange: setGlobalFilter,168 onSortingChange: setSorting,169 renderBottomToolbarCustomActions: () => (170 <Typography>171 Fetched {totalFetched} of {totalDBRowCount} total rows.172 </Typography>173 ),174 state: {175 columnFilters,176 globalFilter,177 isLoading,178 showAlertBanner: isError,179 showProgressBars: isFetching,180 sorting,181 },182 rowVirtualizerInstanceRef, //get access to the virtualizer instance183 rowVirtualizerOptions: { overscan: 4 },184 });185186 return <MaterialReactTable table={table} />;187};188189const queryClient = new QueryClient();190191const ExampleWithReactQueryProvider = () => (192 //App.tsx or AppProviders file. Don't just wrap this component with QueryClientProvider! Wrap your whole App!193 <QueryClientProvider client={queryClient}>194 <Example />195 </QueryClientProvider>196);197198export default ExampleWithReactQueryProvider;199
View Extra Storybook Examples