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_Virtualizer,
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 =
68 useRef<MRT_Virtualizer<HTMLDivElement, HTMLTableRowElement>>(null); //we can get access to the underlying Virtualizer instance and call its scrollToIndex method
69
70 const [columnFilters, setColumnFilters] = useState<MRT_ColumnFiltersState>(
71 [],
72 );
73 const [globalFilter, setGlobalFilter] = useState<string>();
74 const [sorting, setSorting] = useState<MRT_SortingState>([]);
75
76 const { data, fetchNextPage, isError, isFetching, isLoading } =
77 useInfiniteQuery<UserApiResponse>({
78 queryKey: [
79 'table-data',
80 columnFilters, //refetch when columnFilters changes
81 globalFilter, //refetch when globalFilter changes
82 sorting, //refetch when sorting changes
83 ],
84 queryFn: async ({ pageParam }) => {
85 const url = new URL(
86 '/api/data',
87 process.env.NODE_ENV === 'production'
88 ? 'https://www.material-react-table.com'
89 : 'http://localhost:3000',
90 );
91 url.searchParams.set('start', `${(pageParam as number) * fetchSize}`);
92 url.searchParams.set('size', `${fetchSize}`);
93 url.searchParams.set('filters', JSON.stringify(columnFilters ?? []));
94 url.searchParams.set('globalFilter', globalFilter ?? '');
95 url.searchParams.set('sorting', JSON.stringify(sorting ?? []));
96
97 const response = await fetch(url.href);
98 const json = (await response.json()) as UserApiResponse;
99 return json;
100 },
101 initialPageParam: 0,
102 getNextPageParam: (_lastGroup, groups) => groups.length,
103 refetchOnWindowFocus: false,
104 });
105
106 const flatData = useMemo(
107 () => data?.pages.flatMap((page) => page.data) ?? [],
108 [data],
109 );
110
111 const totalDBRowCount = data?.pages?.[0]?.meta?.totalRowCount ?? 0;
112 const totalFetched = flatData.length;
113
114 //called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table
115 const fetchMoreOnBottomReached = useCallback(
116 (containerRefElement?: HTMLDivElement | null) => {
117 if (containerRefElement) {
118 const { scrollHeight, scrollTop, clientHeight } = containerRefElement;
119 //once the user has scrolled within 400px of the bottom of the table, fetch more data if we can
120 if (
121 scrollHeight - scrollTop - clientHeight < 400 &&
122 !isFetching &&
123 totalFetched < totalDBRowCount
124 ) {
125 fetchNextPage();
126 }
127 }
128 },
129 [fetchNextPage, isFetching, totalFetched, totalDBRowCount],
130 );
131
132 //scroll to top of table when sorting or filters change
133 useEffect(() => {
134 //scroll to the top of the table when the sorting changes
135 try {
136 rowVirtualizerInstanceRef.current?.scrollToIndex?.(0);
137 } catch (error) {
138 console.error(error);
139 }
140 }, [sorting, columnFilters, globalFilter]);
141
142 //a check on mount to see if the table is already scrolled to the bottom and immediately needs to fetch more data
143 useEffect(() => {
144 fetchMoreOnBottomReached(tableContainerRef.current);
145 }, [fetchMoreOnBottomReached]);
146
147 const table = useMaterialReactTable({
148 columns,
149 data: flatData,
150 enablePagination: false,
151 enableRowNumbers: true,
152 enableRowVirtualization: true,
153 manualFiltering: true,
154 manualSorting: true,
155 muiTableContainerProps: {
156 ref: tableContainerRef, //get access to the table container element
157 sx: { maxHeight: '600px' }, //give the table a max height
158 onScroll: (event: UIEvent<HTMLDivElement>) =>
159 fetchMoreOnBottomReached(event.target as HTMLDivElement), //add an event listener to the table container element
160 },
161 muiToolbarAlertBannerProps: isError
162 ? {
163 color: 'error',
164 children: 'Error loading data',
165 }
166 : undefined,
167 onColumnFiltersChange: setColumnFilters,
168 onGlobalFilterChange: setGlobalFilter,
169 onSortingChange: setSorting,
170 renderBottomToolbarCustomActions: () => (
171 <Typography>
172 Fetched {totalFetched} of {totalDBRowCount} total rows.
173 </Typography>
174 ),
175 state: {
176 columnFilters,
177 globalFilter,
178 isLoading,
179 showAlertBanner: isError,
180 showProgressBars: isFetching,
181 sorting,
182 },
183 rowVirtualizerInstanceRef, //get access to the virtualizer instance
184 rowVirtualizerOptions: { overscan: 4 },
185 });
186
187 return <MaterialReactTable table={table} />;
188};
189
190const queryClient = new QueryClient();
191
192const ExampleWithReactQueryProvider = () => (
193 //App.tsx or AppProviders file. Don't just wrap this component with QueryClientProvider! Wrap your whole App!
194 <QueryClientProvider client={queryClient}>
195 <Example />
196 </QueryClientProvider>
197);
198
199export default ExampleWithReactQueryProvider;
200

View Extra Storybook Examples