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 UIEvent,3 useCallback,4 useEffect,5 useMemo,6 useRef,7 useState,8} from 'react';9import MaterialReactTable, {10 MRT_ColumnDef,11 MRT_ColumnFiltersState,12 MRT_SortingState,13 MRT_Virtualizer,14} from 'material-react-table';15import { Typography } from '@mui/material';16import {17 QueryClient,18 QueryClientProvider,19 useInfiniteQuery,20} from '@tanstack/react-query';2122type UserApiResponse = {23 data: Array<User>;24 meta: {25 totalRowCount: number;26 };27};2829type User = {30 firstName: string;31 lastName: string;32 address: string;33 state: string;34 phoneNumber: string;35};3637const columns: MRT_ColumnDef<User>[] = [38 {39 accessorKey: 'firstName',40 header: 'First Name',41 },42 {43 accessorKey: 'lastName',44 header: 'Last Name',45 },46 {47 accessorKey: 'address',48 header: 'Address',49 },50 {51 accessorKey: 'state',52 header: 'State',53 },54 {55 accessorKey: 'phoneNumber',56 header: 'Phone Number',57 },58];5960const fetchSize = 25;6162const Example = () => {63 const tableContainerRef = useRef<HTMLDivElement>(null); //we can get access to the underlying TableContainer element and react to its scroll events64 const rowVirtualizerInstanceRef =65 useRef<MRT_Virtualizer<HTMLDivElement, HTMLTableRowElement>>(null); //we can get access to the underlying Virtualizer instance and call its scrollToIndex method6667 const [columnFilters, setColumnFilters] = useState<MRT_ColumnFiltersState>(68 [],69 );70 const [globalFilter, setGlobalFilter] = useState<string>();71 const [sorting, setSorting] = useState<MRT_SortingState>([]);7273 const { data, fetchNextPage, isError, isFetching, isLoading } =74 useInfiniteQuery<UserApiResponse>({75 queryKey: ['table-data', columnFilters, globalFilter, sorting],76 queryFn: async ({ pageParam = 0 }) => {77 const url = new URL(78 '/api/data',79 process.env.NODE_ENV === 'production'80 ? 'https://www.material-react-table.com'81 : 'http://localhost:3000',82 );83 url.searchParams.set('start', `${pageParam * fetchSize}`);84 url.searchParams.set('size', `${fetchSize}`);85 url.searchParams.set('filters', JSON.stringify(columnFilters ?? []));86 url.searchParams.set('globalFilter', globalFilter ?? '');87 url.searchParams.set('sorting', JSON.stringify(sorting ?? []));8889 const response = await fetch(url.href);90 const json = (await response.json()) as UserApiResponse;91 return json;92 },93 getNextPageParam: (_lastGroup, groups) => groups.length,94 keepPreviousData: true,95 refetchOnWindowFocus: false,96 });9798 const flatData = useMemo(99 () => data?.pages.flatMap((page) => page.data) ?? [],100 [data],101 );102103 const totalDBRowCount = data?.pages?.[0]?.meta?.totalRowCount ?? 0;104 const totalFetched = flatData.length;105106 //called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table107 const fetchMoreOnBottomReached = useCallback(108 (containerRefElement?: HTMLDivElement | null) => {109 if (containerRefElement) {110 const { scrollHeight, scrollTop, clientHeight } = containerRefElement;111 //once the user has scrolled within 400px of the bottom of the table, fetch more data if we can112 if (113 scrollHeight - scrollTop - clientHeight < 400 &&114 !isFetching &&115 totalFetched < totalDBRowCount116 ) {117 fetchNextPage();118 }119 }120 },121 [fetchNextPage, isFetching, totalFetched, totalDBRowCount],122 );123124 //scroll to top of table when sorting or filters change125 useEffect(() => {126 if (rowVirtualizerInstanceRef.current) {127 rowVirtualizerInstanceRef.current.scrollToIndex(0);128 }129 }, [sorting, columnFilters, globalFilter]);130131 //a check on mount to see if the table is already scrolled to the bottom and immediately needs to fetch more data132 useEffect(() => {133 fetchMoreOnBottomReached(tableContainerRef.current);134 }, [fetchMoreOnBottomReached]);135136 return (137 <MaterialReactTable138 columns={columns}139 data={flatData}140 enablePagination={false}141 enableRowNumbers142 enableRowVirtualization //optional, but recommended if it is likely going to be more than 100 rows143 manualFiltering144 manualSorting145 muiTableContainerProps={{146 ref: tableContainerRef, //get access to the table container element147 sx: { maxHeight: '600px' }, //give the table a max height148 onScroll: (149 event: UIEvent<HTMLDivElement>, //add an event listener to the table container element150 ) => fetchMoreOnBottomReached(event.target as HTMLDivElement),151 }}152 muiToolbarAlertBannerProps={153 isError154 ? {155 color: 'error',156 children: 'Error loading data',157 }158 : undefined159 }160 onColumnFiltersChange={setColumnFilters}161 onGlobalFilterChange={setGlobalFilter}162 onSortingChange={setSorting}163 renderBottomToolbarCustomActions={() => (164 <Typography>165 Fetched {totalFetched} of {totalDBRowCount} total rows.166 </Typography>167 )}168 state={{169 columnFilters,170 globalFilter,171 isLoading,172 showAlertBanner: isError,173 showProgressBars: isFetching,174 sorting,175 }}176 rowVirtualizerInstanceRef={rowVirtualizerInstanceRef} //get access to the virtualizer instance177 rowVirtualizerProps={{ overscan: 4 }}178 />179 );180};181182const queryClient = new QueryClient();183184const ExampleWithReactQueryProvider = () => (185 <QueryClientProvider client={queryClient}>186 <Example />187 </QueryClientProvider>188);189190export default ExampleWithReactQueryProvider;191
View Extra Storybook Examples