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.


Fetched 0 of 0 total rows.

Source Code

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

View Extra Storybook Examples