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 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 V5
23
24//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};
31
32type User = {
33 firstName: string;
34 lastName: string;
35 address: string;
36 state: string;
37 phoneNumber: string;
38};
39
40const 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];
62
63const fetchSize = 25;
64
65const Example = () => {
66 const tableContainerRef = useRef<HTMLDivElement>(null); //we can get access to the underlying TableContainer element and react to its scroll events
67 const rowVirtualizerInstanceRef = useRef<MRT_RowVirtualizer>(null); //we can get access to the underlying Virtualizer instance and call its scrollToIndex method
68 const [currentMaxPages, setCurrentMaxPages] = useState(4);
69 const [columnFilters, setColumnFilters] = useState<MRT_ColumnFiltersState>(
70 [],
71 );
72 const [globalFilter, setGlobalFilter] = useState<string>();
73 const [sorting, setSorting] = useState<MRT_SortingState>([]);
74
75 const { data, fetchNextPage, isError, isFetching, isLoading } =
76 useInfiniteQuery<UserApiResponse>({
77 queryKey: [
78 'table-data',
79 columnFilters, //refetch when columnFilters changes
80 globalFilter, //refetch when globalFilter changes
81 sorting, //refetch when sorting changes
82 ],
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 ?? []));
95
96 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 maxPages: currentMaxPages,
104 });
105
106 const flatData = useMemo(
107 () => data?.pages.flatMap((page) => page.data) ?? [],
108 [data],
109 );
110
111 const totalDBRowCount = data?.pages?.[0]?.meta?.totalRowCount ?? 0;
112 const totalFetched = flatData.length;
113
114 const handleScroll = useCallback(() => {
115 const containerElement = tableContainerRef.current;
116 if (containerElement) {
117 const { scrollHeight, scrollTop, clientHeight } = containerElement;
118
119 if (
120 scrollHeight - scrollTop - clientHeight < 400 &&
121 !isFetching &&
122 totalFetched < totalDBRowCount &&
123 currentMaxPages < totalDBRowCount / fetchSize
124 ) {
125 setCurrentMaxPages((prevMaxPages) => prevMaxPages + 2);
126 }
127 }
128 }, [currentMaxPages, isFetching, totalFetched, totalDBRowCount]);
129
130 //called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table
131 const fetchMoreOnBottomReached = useCallback(
132 (containerRefElement?: HTMLDivElement | null) => {
133 if (containerRefElement) {
134 const { scrollHeight, scrollTop, clientHeight } = containerRefElement;
135 //once the user has scrolled within 400px of the bottom of the table, fetch more data if we can
136 if (
137 scrollHeight - scrollTop - clientHeight < 400 &&
138 !isFetching &&
139 totalFetched < totalDBRowCount
140 ) {
141 fetchNextPage();
142 }
143 }
144 },
145 [fetchNextPage, isFetching, totalFetched, totalDBRowCount],
146 );
147
148 //scroll to top of table when sorting or filters change
149 useEffect(() => {
150 //scroll to the top of the table when the sorting changes
151 try {
152 rowVirtualizerInstanceRef.current?.scrollToIndex?.(0);
153 } catch (error) {
154 console.error(error);
155 }
156 }, [sorting, columnFilters, globalFilter]);
157
158 useEffect(() => {
159 const containerElement = tableContainerRef.current;
160 if (containerElement) {
161 containerElement.addEventListener('scroll', handleScroll);
162 return () => {
163 containerElement.removeEventListener('scroll', handleScroll);
164 };
165 }
166 }, [handleScroll]);
167
168 //a check on mount to see if the table is already scrolled to the bottom and immediately needs to fetch more data
169 useEffect(() => {
170 fetchMoreOnBottomReached(tableContainerRef.current);
171 }, [fetchMoreOnBottomReached]);
172
173 const table = useMaterialReactTable({
174 columns,
175 data: flatData,
176 enablePagination: false,
177 enableRowNumbers: true,
178 enableRowVirtualization: true,
179 manualFiltering: true,
180 manualSorting: true,
181 muiTableContainerProps: {
182 ref: tableContainerRef, //get access to the table container element
183 sx: { maxHeight: '600px' }, //give the table a max height
184 onScroll: (event: UIEvent<HTMLDivElement>) =>
185 fetchMoreOnBottomReached(event.target as HTMLDivElement), //add an event listener to the table container element
186 },
187 muiToolbarAlertBannerProps: isError
188 ? {
189 color: 'error',
190 children: 'Error loading data',
191 }
192 : undefined,
193 onColumnFiltersChange: setColumnFilters,
194 onGlobalFilterChange: setGlobalFilter,
195 onSortingChange: setSorting,
196 renderBottomToolbarCustomActions: () => (
197 <Typography>
198 Fetched {totalFetched} of {totalDBRowCount} total rows.
199 </Typography>
200 ),
201 state: {
202 columnFilters,
203 globalFilter,
204 isLoading,
205 showAlertBanner: isError,
206 showProgressBars: isFetching,
207 sorting,
208 },
209 rowVirtualizerInstanceRef, //get access to the virtualizer instance
210 rowVirtualizerOptions: { overscan: 4 },
211 });
212
213 return <MaterialReactTable table={table} />;
214};
215
216const queryClient = new QueryClient();
217
218const ExampleWithReactQueryProvider = () => (
219 //App.tsx or AppProviders file. Don't just wrap this component with QueryClientProvider! Wrap your whole App!
220 <QueryClientProvider client={queryClient}>
221 <Example />
222 </QueryClientProvider>
223);
224
225export default ExampleWithReactQueryProvider;
226

View Extra Storybook Examples