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.

More Examples

Demo

Open StackblitzOpen Code SandboxOpen on GitHub

Fetched 0 of 0 total rows.

Source Code

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

View Extra Storybook Examples