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

View Extra Storybook Examples