MRT logoMaterial React Table

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.

More Examples

Demo

Open StackblitzOpen Code SandboxOpen on GitHub

Fetched 0 of 0 total rows.

Source Code

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 V5
25
26//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};
33
34type User = {
35 firstName: string;
36 lastName: string;
37 address: string;
38 state: string;
39 phoneNumber: string;
40};
41
42const 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];
64
65const fetchSize = 25;
66
67const Example = () => {
68 const tableContainerRef = useRef<HTMLDivElement>(null); //we can get access to the underlying TableContainer element and react to its scroll events
69 const rowVirtualizerInstanceRef = useRef<MRT_RowVirtualizer>(null); //we can get access to the underlying Virtualizer instance and call its scrollToIndex method
70
71 const [columnFilters, setColumnFilters] = useState<MRT_ColumnFiltersState>(
72 [],
73 );
74 const [globalFilter, setGlobalFilter] = useState<string>();
75 const [sorting, setSorting] = useState<MRT_SortingState>([]);
76
77 const { data, fetchNextPage, isError, isFetching, isLoading } =
78 useInfiniteQuery<UserApiResponse>({
79 queryKey: [
80 'users-list',
81 {
82 columnFilters, //refetch when columnFilters changes
83 globalFilter, //refetch when globalFilter changes
84 sorting, //refetch when sorting changes
85 },
86 ],
87 queryFn: async ({ pageParam }) => {
88 const url = new URL('/api/data', location.origin); // nextjs api route
89 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 ?? []));
94
95 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 });
103
104 const flatData = useMemo(
105 () => data?.pages.flatMap((page) => page.data) ?? [],
106 [data],
107 );
108
109 const totalDBRowCount = data?.pages?.[0]?.meta?.totalRowCount ?? 0;
110 const totalFetched = flatData.length;
111
112 //called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table
113 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 can
118 if (
119 scrollHeight - scrollTop - clientHeight < 400 &&
120 !isFetching &&
121 totalFetched < totalDBRowCount
122 ) {
123 fetchNextPage();
124 }
125 }
126 },
127 [fetchNextPage, isFetching, totalFetched, totalDBRowCount],
128 );
129
130 //scroll to top of table when sorting or filters change
131 useEffect(() => {
132 //scroll to the top of the table when the sorting changes
133 try {
134 rowVirtualizerInstanceRef.current?.scrollToIndex?.(0);
135 } catch (error) {
136 console.error(error);
137 }
138 }, [sorting, columnFilters, globalFilter]);
139
140 //a check on mount to see if the table is already scrolled to the bottom and immediately needs to fetch more data
141 useEffect(() => {
142 fetchMoreOnBottomReached(tableContainerRef.current);
143 }, [fetchMoreOnBottomReached]);
144
145 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 element
155 sx: { maxHeight: '600px' }, //give the table a max height
156 onScroll: (event: UIEvent<HTMLDivElement>) =>
157 fetchMoreOnBottomReached(event.target as HTMLDivElement), //add an event listener to the table container element
158 },
159 muiToolbarAlertBannerProps: isError
160 ? {
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 instance
182 rowVirtualizerOptions: { overscan: 4 },
183 });
184
185 return <MaterialReactTable table={table} />;
186};
187
188//react query setup in App.tsx
189const ReactQueryDevtoolsProduction = lazy(() =>
190 import('@tanstack/react-query-devtools/build/modern/production.js').then(
191 (d) => ({
192 default: d.ReactQueryDevtools,
193 }),
194 ),
195);
196
197const queryClient = new QueryClient();
198
199export 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