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.


Demo

Open StackblitzOpen Code SandboxOpen on GitHub

Fetched 0 of 0 total rows.

Source Code

1import React, {
2 UIEvent,
3 useCallback,
4 useEffect,
5 useMemo,
6 useRef,
7 useState,
8} from 'react';
9import {
10 MaterialReactTable,
11 type MRT_ColumnDef,
12 type MRT_ColumnFiltersState,
13 type MRT_SortingState,
14 type MRT_Virtualizer,
15} from 'material-react-table';
16import { Typography } from '@mui/material';
17import {
18 QueryClient,
19 QueryClientProvider,
20 useInfiniteQuery,
21} from '@tanstack/react-query';
22
23type UserApiResponse = {
24 data: Array<User>;
25 meta: {
26 totalRowCount: number;
27 };
28};
29
30type User = {
31 firstName: string;
32 lastName: string;
33 address: string;
34 state: string;
35 phoneNumber: string;
36};
37
38const columns: MRT_ColumnDef<User>[] = [
39 {
40 accessorKey: 'firstName',
41 header: 'First Name',
42 },
43 {
44 accessorKey: 'lastName',
45 header: 'Last Name',
46 },
47 {
48 accessorKey: 'address',
49 header: 'Address',
50 },
51 {
52 accessorKey: 'state',
53 header: 'State',
54 },
55 {
56 accessorKey: 'phoneNumber',
57 header: 'Phone Number',
58 },
59];
60
61const fetchSize = 25;
62
63const Example = () => {
64 const tableContainerRef = useRef<HTMLDivElement>(null); //we can get access to the underlying TableContainer element and react to its scroll events
65 const rowVirtualizerInstanceRef =
66 useRef<MRT_Virtualizer<HTMLDivElement, HTMLTableRowElement>>(null); //we can get access to the underlying Virtualizer instance and call its scrollToIndex method
67
68 const [columnFilters, setColumnFilters] = useState<MRT_ColumnFiltersState>(
69 [],
70 );
71 const [globalFilter, setGlobalFilter] = useState<string>();
72 const [sorting, setSorting] = useState<MRT_SortingState>([]);
73
74 const { data, fetchNextPage, isError, isFetching, isLoading } =
75 useInfiniteQuery<UserApiResponse>({
76 queryKey: ['table-data', columnFilters, globalFilter, sorting],
77 queryFn: async ({ pageParam = 0 }) => {
78 const url = new URL(
79 '/api/data',
80 process.env.NODE_ENV === 'production'
81 ? 'https://www.material-react-table.com'
82 : 'http://localhost:3000',
83 );
84 url.searchParams.set('start', `${pageParam * fetchSize}`);
85 url.searchParams.set('size', `${fetchSize}`);
86 url.searchParams.set('filters', JSON.stringify(columnFilters ?? []));
87 url.searchParams.set('globalFilter', globalFilter ?? '');
88 url.searchParams.set('sorting', JSON.stringify(sorting ?? []));
89
90 const response = await fetch(url.href);
91 const json = (await response.json()) as UserApiResponse;
92 return json;
93 },
94 getNextPageParam: (_lastGroup, groups) => groups.length,
95 keepPreviousData: true,
96 refetchOnWindowFocus: false,
97 });
98
99 const flatData = useMemo(
100 () => data?.pages.flatMap((page) => page.data) ?? [],
101 [data],
102 );
103
104 const totalDBRowCount = data?.pages?.[0]?.meta?.totalRowCount ?? 0;
105 const totalFetched = flatData.length;
106
107 //called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table
108 const fetchMoreOnBottomReached = useCallback(
109 (containerRefElement?: HTMLDivElement | null) => {
110 if (containerRefElement) {
111 const { scrollHeight, scrollTop, clientHeight } = containerRefElement;
112 //once the user has scrolled within 400px of the bottom of the table, fetch more data if we can
113 if (
114 scrollHeight - scrollTop - clientHeight < 400 &&
115 !isFetching &&
116 totalFetched < totalDBRowCount
117 ) {
118 fetchNextPage();
119 }
120 }
121 },
122 [fetchNextPage, isFetching, totalFetched, totalDBRowCount],
123 );
124
125 //scroll to top of table when sorting or filters change
126 useEffect(() => {
127 //scroll to the top of the table when the sorting changes
128 try {
129 rowVirtualizerInstanceRef.current?.scrollToIndex?.(0);
130 } catch (error) {
131 console.error(error);
132 }
133 }, [sorting, columnFilters, globalFilter]);
134
135 //a check on mount to see if the table is already scrolled to the bottom and immediately needs to fetch more data
136 useEffect(() => {
137 fetchMoreOnBottomReached(tableContainerRef.current);
138 }, [fetchMoreOnBottomReached]);
139
140 return (
141 <MaterialReactTable
142 columns={columns}
143 data={flatData}
144 enablePagination={false}
145 enableRowNumbers
146 enableRowVirtualization //optional, but recommended if it is likely going to be more than 100 rows
147 manualFiltering
148 manualSorting
149 muiTableContainerProps={{
150 ref: tableContainerRef, //get access to the table container element
151 sx: { maxHeight: '600px' }, //give the table a max height
152 onScroll: (
153 event: UIEvent<HTMLDivElement>, //add an event listener to the table container element
154 ) => fetchMoreOnBottomReached(event.target as HTMLDivElement),
155 }}
156 muiToolbarAlertBannerProps={
157 isError
158 ? {
159 color: 'error',
160 children: 'Error loading data',
161 }
162 : undefined
163 }
164 onColumnFiltersChange={setColumnFilters}
165 onGlobalFilterChange={setGlobalFilter}
166 onSortingChange={setSorting}
167 renderBottomToolbarCustomActions={() => (
168 <Typography>
169 Fetched {totalFetched} of {totalDBRowCount} total rows.
170 </Typography>
171 )}
172 state={{
173 columnFilters,
174 globalFilter,
175 isLoading,
176 showAlertBanner: isError,
177 showProgressBars: isFetching,
178 sorting,
179 }}
180 rowVirtualizerInstanceRef={rowVirtualizerInstanceRef} //get access to the virtualizer instance
181 rowVirtualizerProps={{ overscan: 4 }}
182 />
183 );
184};
185
186const queryClient = new QueryClient();
187
188const ExampleWithReactQueryProvider = () => (
189 <QueryClientProvider client={queryClient}>
190 <Example />
191 </QueryClientProvider>
192);
193
194export default ExampleWithReactQueryProvider;
195

View Extra Storybook Examples