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_Virtualizer,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 =68 useRef<MRT_Virtualizer<HTMLDivElement, HTMLTableRowElement>>(null); //we can get access to the underlying Virtualizer instance and call its scrollToIndex method6970 const [columnFilters, setColumnFilters] = useState<MRT_ColumnFiltersState>(71 [],72 );73 const [globalFilter, setGlobalFilter] = useState<string>();74 const [sorting, setSorting] = useState<MRT_SortingState>([]);7576 const { data, fetchNextPage, isError, isFetching, isLoading } =77 useInfiniteQuery<UserApiResponse>({78 queryKey: [79 'table-data',80 columnFilters, //refetch when columnFilters changes81 globalFilter, //refetch when globalFilter changes82 sorting, //refetch when sorting changes83 ],84 queryFn: async ({ pageParam }) => {85 const url = new URL(86 '/api/data',87 process.env.NODE_ENV === 'production'88 ? 'https://www.material-react-table.com'89 : 'http://localhost:3000',90 );91 url.searchParams.set('start', `${(pageParam as number) * fetchSize}`);92 url.searchParams.set('size', `${fetchSize}`);93 url.searchParams.set('filters', JSON.stringify(columnFilters ?? []));94 url.searchParams.set('globalFilter', globalFilter ?? '');95 url.searchParams.set('sorting', JSON.stringify(sorting ?? []));9697 const response = await fetch(url.href);98 const json = (await response.json()) as UserApiResponse;99 return json;100 },101 initialPageParam: 0,102 getNextPageParam: (_lastGroup, groups) => groups.length,103 refetchOnWindowFocus: false,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 //called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table115 const fetchMoreOnBottomReached = useCallback(116 (containerRefElement?: HTMLDivElement | null) => {117 if (containerRefElement) {118 const { scrollHeight, scrollTop, clientHeight } = containerRefElement;119 //once the user has scrolled within 400px of the bottom of the table, fetch more data if we can120 if (121 scrollHeight - scrollTop - clientHeight < 400 &&122 !isFetching &&123 totalFetched < totalDBRowCount124 ) {125 fetchNextPage();126 }127 }128 },129 [fetchNextPage, isFetching, totalFetched, totalDBRowCount],130 );131132 //scroll to top of table when sorting or filters change133 useEffect(() => {134 //scroll to the top of the table when the sorting changes135 try {136 rowVirtualizerInstanceRef.current?.scrollToIndex?.(0);137 } catch (error) {138 console.error(error);139 }140 }, [sorting, columnFilters, globalFilter]);141142 //a check on mount to see if the table is already scrolled to the bottom and immediately needs to fetch more data143 useEffect(() => {144 fetchMoreOnBottomReached(tableContainerRef.current);145 }, [fetchMoreOnBottomReached]);146147 const table = useMaterialReactTable({148 columns,149 data: flatData,150 enablePagination: false,151 enableRowNumbers: true,152 enableRowVirtualization: true,153 manualFiltering: true,154 manualSorting: true,155 muiTableContainerProps: {156 ref: tableContainerRef, //get access to the table container element157 sx: { maxHeight: '600px' }, //give the table a max height158 onScroll: (event: UIEvent<HTMLDivElement>) =>159 fetchMoreOnBottomReached(event.target as HTMLDivElement), //add an event listener to the table container element160 },161 muiToolbarAlertBannerProps: isError162 ? {163 color: 'error',164 children: 'Error loading data',165 }166 : undefined,167 onColumnFiltersChange: setColumnFilters,168 onGlobalFilterChange: setGlobalFilter,169 onSortingChange: setSorting,170 renderBottomToolbarCustomActions: () => (171 <Typography>172 Fetched {totalFetched} of {totalDBRowCount} total rows.173 </Typography>174 ),175 state: {176 columnFilters,177 globalFilter,178 isLoading,179 showAlertBanner: isError,180 showProgressBars: isFetching,181 sorting,182 },183 rowVirtualizerInstanceRef, //get access to the virtualizer instance184 rowVirtualizerOptions: { overscan: 4 },185 });186187 return <MaterialReactTable table={table} />;188};189190const queryClient = new QueryClient();191192const ExampleWithReactQueryProvider = () => (193 //App.tsx or AppProviders file. Don't just wrap this component with QueryClientProvider! Wrap your whole App!194 <QueryClientProvider client={queryClient}>195 <Example />196 </QueryClientProvider>197);198199export default ExampleWithReactQueryProvider;200
View Extra Storybook Examples