React Example: Infinite Scroll

import React from 'react' import ReactDOM from 'react-dom' import { QueryClient, QueryClientProvider, useInfiniteQuery, } from '@tanstack/react-query' import './index.css' import { useVirtualizer } from '@tanstack/react-virtual' const queryClient = new QueryClient() async function fetchServerPage( limit: number, offset: number = 0, ): Promise<{ rows: Array<string>; nextOffset: number }> { const rows = new Array(limit) .fill(0) .map((_, i) => `Async loaded row #${i + offset * limit}`) await new Promise((r) => setTimeout(r, 500)) return { rows, nextOffset: offset + 1 } } function App() { const { status, data, error, isFetching, isFetchingNextPage, fetchNextPage, hasNextPage, } = useInfiniteQuery({ queryKey: ['projects'], queryFn: (ctx) => fetchServerPage(10, ctx.pageParam), getNextPageParam: (lastGroup) => lastGroup.nextOffset, initialPageParam: 0, }) const allRows = data ? data.pages.flatMap((d) => d.rows) : [] const parentRef = React.useRef<HTMLDivElement>(null) const rowVirtualizer = useVirtualizer({ count: hasNextPage ? allRows.length + 1 : allRows.length, getScrollElement: () => parentRef.current, estimateSize: () => 100, overscan: 5, }) React.useEffect(() => { const [lastItem] = [...rowVirtualizer.getVirtualItems()].reverse() if (!lastItem) { return } if ( lastItem.index >= allRows.length - 1 && hasNextPage && !isFetchingNextPage ) { fetchNextPage() } }, [ hasNextPage, fetchNextPage, allRows.length, isFetchingNextPage, rowVirtualizer.getVirtualItems(), ]) return ( <div> <p> This infinite scroll example uses React Query's useInfiniteScroll hook to fetch infinite data from a posts endpoint and then a rowVirtualizer is used along with a loader-row placed at the bottom of the list to trigger the next page to load. </p> <br /> <br /> {status === 'pending' ? ( <p>Loading...</p> ) : status === 'error' ? ( <span>Error: {error.message}</span> ) : ( <div ref={parentRef} className="List" style={{ height: `500px`, width: `100%`, overflow: 'auto', }} > <div style={{ height: `${rowVirtualizer.getTotalSize()}px`, width: '100%', position: 'relative', }} > {rowVirtualizer.getVirtualItems().map((virtualRow) => { const isLoaderRow = virtualRow.index > allRows.length - 1 const post = allRows[virtualRow.index] return ( <div key={virtualRow.index} className={ virtualRow.index % 2 ? 'ListItemOdd' : 'ListItemEven' } style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: `${virtualRow.size}px`, transform: `translateY(${virtualRow.start}px)`, }} > {isLoaderRow ? hasNextPage ? 'Loading more...' : 'Nothing more to load' : post} </div> ) })} </div> </div> )} <div> {isFetching && !isFetchingNextPage ? 'Background Updating...' : null} </div> <br /> <br /> {process.env.NODE_ENV === 'development' ? ( <p> <strong>Notice:</strong> You are currently running React in development mode. Rendering performance will be slightly degraded until this application is built for production. </p> ) : null} </div> ) } ReactDOM.render( <React.StrictMode> <QueryClientProvider client={queryClient}> <App /> </QueryClientProvider> </React.StrictMode>, document.getElementById('root'), ) 
import React from 'react' import ReactDOM from 'react-dom' import { QueryClient, QueryClientProvider, useInfiniteQuery, } from '@tanstack/react-query' import './index.css' import { useVirtualizer } from '@tanstack/react-virtual' const queryClient = new QueryClient() async function fetchServerPage( limit: number, offset: number = 0, ): Promise<{ rows: Array<string>; nextOffset: number }> { const rows = new Array(limit) .fill(0) .map((_, i) => `Async loaded row #${i + offset * limit}`) await new Promise((r) => setTimeout(r, 500)) return { rows, nextOffset: offset + 1 } } function App() { const { status, data, error, isFetching, isFetchingNextPage, fetchNextPage, hasNextPage, } = useInfiniteQuery({ queryKey: ['projects'], queryFn: (ctx) => fetchServerPage(10, ctx.pageParam), getNextPageParam: (lastGroup) => lastGroup.nextOffset, initialPageParam: 0, }) const allRows = data ? data.pages.flatMap((d) => d.rows) : [] const parentRef = React.useRef<HTMLDivElement>(null) const rowVirtualizer = useVirtualizer({ count: hasNextPage ? allRows.length + 1 : allRows.length, getScrollElement: () => parentRef.current, estimateSize: () => 100, overscan: 5, }) React.useEffect(() => { const [lastItem] = [...rowVirtualizer.getVirtualItems()].reverse() if (!lastItem) { return } if ( lastItem.index >= allRows.length - 1 && hasNextPage && !isFetchingNextPage ) { fetchNextPage() } }, [ hasNextPage, fetchNextPage, allRows.length, isFetchingNextPage, rowVirtualizer.getVirtualItems(), ]) return ( <div> <p> This infinite scroll example uses React Query's useInfiniteScroll hook to fetch infinite data from a posts endpoint and then a rowVirtualizer is used along with a loader-row placed at the bottom of the list to trigger the next page to load. </p> <br /> <br /> {status === 'pending' ? ( <p>Loading...</p> ) : status === 'error' ? ( <span>Error: {error.message}</span> ) : ( <div ref={parentRef} className="List" style={{ height: `500px`, width: `100%`, overflow: 'auto', }} > <div style={{ height: `${rowVirtualizer.getTotalSize()}px`, width: '100%', position: 'relative', }} > {rowVirtualizer.getVirtualItems().map((virtualRow) => { const isLoaderRow = virtualRow.index > allRows.length - 1 const post = allRows[virtualRow.index] return ( <div key={virtualRow.index} className={ virtualRow.index % 2 ? 'ListItemOdd' : 'ListItemEven' } style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: `${virtualRow.size}px`, transform: `translateY(${virtualRow.start}px)`, }} > {isLoaderRow ? hasNextPage ? 'Loading more...' : 'Nothing more to load' : post} </div> ) })} </div> </div> )} <div> {isFetching && !isFetchingNextPage ? 'Background Updating...' : null} </div> <br /> <br /> {process.env.NODE_ENV === 'development' ? ( <p> <strong>Notice:</strong> You are currently running React in development mode. Rendering performance will be slightly degraded until this application is built for production. </p> ) : null} </div> ) } ReactDOM.render( <React.StrictMode> <QueryClientProvider client={queryClient}> <App /> </QueryClientProvider> </React.StrictMode>, document.getElementById('root'), ) 
Subscribe to Bytes

Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.

Subscribe to Bytes

Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.