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 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';
21
22type UserApiResponse = {
23 data: Array<User>;
24 meta: {
25 totalRowCount: number;
26 };
27};
28
29type User = {
30 firstName: string;
31 lastName: string;
32 address: string;
33 state: string;
34 phoneNumber: string;
35};
36
37const 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];
59
60const fetchSize = 25;
61
62const Example = () => {
63 const tableContainerRef = useRef<HTMLDivElement>(null); //we can get access to the underlying TableContainer element and react to its scroll events
64 const rowVirtualizerInstanceRef =
65 useRef<MRT_Virtualizer<HTMLDivElement, HTMLTableRowElement>>(null); //we can get access to the underlying Virtualizer instance and call its scrollToIndex method
66
67 const [columnFilters, setColumnFilters] = useState<MRT_ColumnFiltersState>(
68 [],
69 );
70 const [globalFilter, setGlobalFilter] = useState<string>();
71 const [sorting, setSorting] = useState<MRT_SortingState>([]);
72
73 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 ?? []));
88
89 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 });
97
98 const flatData = useMemo(
99 () => data?.pages.flatMap((page) => page.data) ?? [],
100 [data],
101 );
102
103 const totalDBRowCount = data?.pages?.[0]?.meta?.totalRowCount ?? 0;
104 const totalFetched = flatData.length;
105
106 //called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table
107 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 can
112 if (
113 scrollHeight - scrollTop - clientHeight < 400 &&
114 !isFetching &&
115 totalFetched < totalDBRowCount
116 ) {
117 fetchNextPage();
118 }
119 }
120 },
121 [fetchNextPage, isFetching, totalFetched, totalDBRowCount],
122 );
123
124 //scroll to top of table when sorting or filters change
125 useEffect(() => {
126 if (rowVirtualizerInstanceRef.current) {
127 rowVirtualizerInstanceRef.current.scrollToIndex(0);
128 }
129 }, [sorting, columnFilters, globalFilter]);
130
131 //a check on mount to see if the table is already scrolled to the bottom and immediately needs to fetch more data
132 useEffect(() => {
133 fetchMoreOnBottomReached(tableContainerRef.current);
134 }, [fetchMoreOnBottomReached]);
135
136 return (
137 <MaterialReactTable
138 columns={columns}
139 data={flatData}
140 enablePagination={false}
141 enableRowNumbers
142 enableRowVirtualization //optional, but recommended if it is likely going to be more than 100 rows
143 manualFiltering
144 manualSorting
145 muiTableContainerProps={{
146 ref: tableContainerRef, //get access to the table container element
147 sx: { maxHeight: '600px' }, //give the table a max height
148 onScroll: (
149 event: UIEvent<HTMLDivElement>, //add an event listener to the table container element
150 ) => fetchMoreOnBottomReached(event.target as HTMLDivElement),
151 }}
152 muiToolbarAlertBannerProps={
153 isError
154 ? {
155 color: 'error',
156 children: 'Error loading data',
157 }
158 : undefined
159 }
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 instance
177 rowVirtualizerProps={{ overscan: 4 }}
178 />
179 );
180};
181
182const queryClient = new QueryClient();
183
184const ExampleWithReactQueryProvider = () => (
185 <QueryClientProvider client={queryClient}>
186 <Example />
187 </QueryClientProvider>
188);
189
190export default ExampleWithReactQueryProvider;
191

View Extra Storybook Examples