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
#
First Name
Last Name
Address
State
Phone Number

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 'https://www.material-react-table.com',
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 { data: axiosData } = await axios.get(url.href);
86 return axiosData;
87 },
88 {
89 getNextPageParam: (_lastGroup, groups) => groups.length,
90 keepPreviousData: true,
91 refetchOnWindowFocus: false,
92 },
93 );
94
95 const flatData = useMemo(
96 () => data?.pages.flatMap((page) => page.data) ?? [],
97 [data],
98 );
99
100 const totalDBRowCount = data?.pages?.[0]?.meta?.totalRowCount ?? 0;
101 const totalFetched = flatData.length;
102
103 //called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table
104 const fetchMoreOnBottomReached = useCallback(
105 (containerRefElement?: HTMLDivElement | null) => {
106 if (containerRefElement) {
107 const { scrollHeight, scrollTop, clientHeight } = containerRefElement;
108 //once the user has scrolled within 200px of the bottom of the table, fetch more data if we can
109 if (
110 scrollHeight - scrollTop - clientHeight < 200 &&
111 !isFetching &&
112 totalFetched < totalDBRowCount
113 ) {
114 fetchNextPage();
115 }
116 }
117 },
118 [fetchNextPage, isFetching, totalFetched, totalDBRowCount],
119 );
120
121 //scroll to top of table when sorting or filters change
122 useEffect(() => {
123 if (virtualizerInstanceRef.current) {
124 virtualizerInstanceRef.current.scrollToIndex(0);
125 }
126 }, [sorting, columnFilters, globalFilter]);
127
128 //a check on mount to see if the table is already scrolled to the bottom and immediately needs to fetch more data
129 useEffect(() => {
130 fetchMoreOnBottomReached(tableContainerRef.current);
131 }, [fetchMoreOnBottomReached]);
132
133 return (
134 <MaterialReactTable
135 columns={columns}
136 data={flatData}
137 enablePagination={false}
138 enableRowNumbers
139 enableRowVirtualization //optional, but recommended if it is likely going to be more than 100 rows
140 manualFiltering
141 manualSorting
142 muiTableContainerProps={{
143 ref: tableContainerRef, //get access to the table container element
144 sx: { maxHeight: '600px' }, //give the table a max height
145 onScroll: (
146 event: UIEvent<HTMLDivElement>, //add an event listener to the table container element
147 ) => fetchMoreOnBottomReached(event.target as HTMLDivElement),
148 }}
149 muiToolbarAlertBannerProps={
150 isError
151 ? {
152 color: 'error',
153 children: 'Error loading data',
154 }
155 : undefined
156 }
157 onColumnFiltersChange={setColumnFilters}
158 onGlobalFilterChange={setGlobalFilter}
159 onSortingChange={setSorting}
160 renderBottomToolbarCustomActions={() => (
161 <Typography>
162 Fetched {totalFetched} of {totalDBRowCount} total rows.
163 </Typography>
164 )}
165 state={{
166 columnFilters,
167 globalFilter,
168 isLoading,
169 showAlertBanner: isError,
170 showProgressBars: isFetching,
171 sorting,
172 }}
173 virtualizerInstanceRef={virtualizerInstanceRef} //get access to the virtualizer instance
174 />
175 );
176};
177
178const queryClient = new QueryClient();
179
180const ExampleWithReactQueryProvider = () => (
181 <QueryClientProvider client={queryClient}>
182 <Example />
183 </QueryClientProvider>
184);
185
186export default ExampleWithReactQueryProvider;
187

View Extra Storybook Examples