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

View Extra Storybook Examples