
Rendering lists that can additively "load more" data onto an existing set of data or "infinite scroll" is also a very common UI pattern. React Query supports a useful version of useQuery called useInfiniteQuery for querying these types of lists.
When using useInfiniteQuery, you'll notice a few things are different:
data is now an object containing infinite query data:data.pages array containing the fetched pagesdata.pageParams array containing the page params used to fetch the pagesfetchNextPage and fetchPreviousPage functions are now availablegetNextPageParam and getPreviousPageParam options are available for both determining if there is more data to load and the information to fetch it. This information is supplied as an additional parameter in the query function (which can optionally be overridden when calling the fetchNextPage or fetchPreviousPage functions)hasNextPage boolean is now available and is true if getNextPageParam returns a value other than undefinedhasPreviousPage boolean is now available and is true if getPreviousPageParam returns a value other than undefinedisFetchingNextPage and isFetchingPreviousPage booleans are now available to distinguish between a background refresh state and a loading more stateNote: When using options like
initialDataorselectin your query, make sure that when you restructure your data that it still includesdata.pagesanddata.pageParamsproperties, otherwise your changes will be overwritten by the query in its return!
Let's assume we have an API that returns pages of projects 3 at a time based on a cursor index along with a cursor that can be used to fetch the next group of projects:
fetch('/api/projects?cursor=0')// { data: [...], nextCursor: 3}fetch('/api/projects?cursor=3')// { data: [...], nextCursor: 6}fetch('/api/projects?cursor=6')// { data: [...], nextCursor: 9}fetch('/api/projects?cursor=9')// { data: [...] }
With this information, we can create a "Load More" UI by:
useInfiniteQuery to request the first group of data by defaultgetNextPageParamfetchNextPage functionNote: It's very important you do not call
fetchNextPagewith arguments unless you want them to override thepageParamdata returned from thegetNextPageParamfunction. e.g. Do not do this:<button onClick={fetchNextPage} />as this would send the onClick event to thefetchNextPagefunction.
import { useInfiniteQuery } from 'react-query'function Projects() {const fetchProjects = ({ pageParam = 0 }) =>fetch('/api/projects?cursor=' + pageParam)const {data,error,fetchNextPage,hasNextPage,isFetching,isFetchingNextPage,status,} = useInfiniteQuery('projects', fetchProjects, {getNextPageParam: (lastPage, pages) => lastPage.nextCursor,})return status === 'loading' ? (<p>Loading...</p>) : status === 'error' ? (<p>Error: {error.message}</p>) : (<>{data.pages.map((group, i) => (<React.Fragment key={i}>{group.projects.map(project => (<p key={project.id}>{project.name}</p>))}</React.Fragment>))}<div><buttononClick={() => fetchNextPage()}disabled={!hasNextPage || isFetchingNextPage}>{isFetchingNextPage? 'Loading more...': hasNextPage? 'Load More': 'Nothing more to load'}</button></div><div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div></>)}
When an infinite query becomes stale and needs to be refetched, each group is fetched sequentially, starting from the first one. This ensures that even if the underlying data is mutated, we're not using stale cursors and potentially getting duplicates or skipping records. If an infinite query's results are ever removed from the queryCache, the pagination restarts at the initial state with only the initial group being requested.
If you only want to actively refetch a subset of all pages, you can pass the refetchPage function to refetch returned from useInfiniteQuery.
const { refetch } = useInfiniteQuery('projects', fetchProjects, {getNextPageParam: (lastPage, pages) => lastPage.nextCursor,})// only refetch the first pagerefetch({ refetchPage: (page, index) => index === 0 })
You can also pass this function as part of the 2nd argument (queryFilters) to queryClient.refetchQueries, queryClient.invalidateQueries or queryClient.resetQueries.
Signature
refetchPage: (page: TData, index: number, allPages: TData[]) => booleanThe function is executed for each page, and only pages where this function returns true will be refetched.
By default, the variable returned from getNextPageParam will be supplied to the query function, but in some cases, you may want to override this. You can pass custom variables to the fetchNextPage function which will override the default variable like so:
function Projects() {const fetchProjects = ({ pageParam = 0 }) =>fetch('/api/projects?cursor=' + pageParam)const {status,data,isFetching,isFetchingNextPage,fetchNextPage,hasNextPage,} = useInfiniteQuery('projects', fetchProjects, {getNextPageParam: (lastPage, pages) => lastPage.nextCursor,})// Pass your own page paramconst skipToCursor50 = () => fetchNextPage({ pageParam: 50 })}
Bi-directional lists can be implemented by using the getPreviousPageParam, fetchPreviousPage, hasPreviousPage and isFetchingPreviousPage properties and functions.
useInfiniteQuery('projects', fetchProjects, {getNextPageParam: (lastPage, pages) => lastPage.nextCursor,getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,})
Sometimes you may want to show the pages in reversed order. If this is case, you can use the select option:
useInfiniteQuery('projects', fetchProjects, {select: data => ({pages: [...data.pages].reverse(),pageParams: [...data.pageParams].reverse(),}),})
Manually removing first page:
queryClient.setQueryData('projects', data => ({pages: data.pages.slice(1),pageParams: data.pageParams.slice(1),}))
Manually removing a single value from an individual page:
const newPagesArray = oldPagesArray?.pages.map((page) =>page.filter((val) => val.id !== updatedId)) ?? []queryClient.setQueryData('projects', data => ({pages: newPagesArray,pageParams: data.pageParams,}))
Make sure to keep the same data structure of pages and pageParams!
The latest TanStack news, articles, and resources, sent to your inbox.