Infinite Queries
Rendering lists that can additively load more
data onto an existing set of data or infinite scroll
is also a very common UI pattern.
Vue 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 pages.data.pageParams
array containing the page params used to fetch the pages.The
fetchNextPage
andfetchPreviousPage
functions are now available.The
getNextPageParam
andgetPreviousPageParam
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 thefetchNextPage
orfetchPreviousPage
functions).A
hasNextPage
boolean is now available and istrue
ifgetNextPageParam
returns a value other thanundefined
.A
hasPreviousPage
boolean is now available and istrue
ifgetPreviousPageParam
returns a value other thanundefined
.The
isFetchingNextPage
andisFetchingPreviousPage
booleans are now available to distinguish between a background refresh state and a loading more state.
TIP
Ensure that when restructuring your data and utilizing options such as initialData
or select
in your query, you include the properties data.pages
and data.pageParams
. Failing to do so will result in the query overwriting your changes upon its return.
Example
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:
Waiting for
useInfiniteQuery
to request the first group of data by default.Returning the information for the next query in
getNextPageParam
Calling
fetchNextPage
function.
DANGER
Note: It's very important you do not call fetchNextPage
with arguments unless you want them to override the pageParam
data returned from the getNextPageParam
function. Do not do this: <button @click={fetchNextPage} />
as this would send the onClick event to the fetchNextPage
function.
<script setup>
import { defineComponent } from "vue";
import { useInfiniteQuery } from "vue-query";
const fetchProjects = ({ pageParam = 0 }) =>
fetch("/api/projects?cursor=" + pageParam);
function useProjectsInfiniteQuery() {
return useInfiniteQuery("projects", fetchProjects, {
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
});
}
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
isLoading,
isError,
} = useProjectsInfiniteQuery();
</script>
<template>
<span v-if="isLoading">Loading...</span>
<span v-else-if="isError">Error: {{ error.message }}</span>
<div v-else>
<span v-if="isFetching && !isFetchingNextPage">Fetching...</span>
<ul v-for="(group, index) in data.pages" :key="index">
<li v-for="project in group.projects" :key="project.id">
{{ project.name }}
</li>
</ul>
<button
@click="() => fetchNextPage()"
:disabled="!hasNextPage || isFetchingNextPage"
>
<span v-if="isFetchingNextPage">Loading more...</span>
<span v-else-if="hasNextPage">Load More</span>
<span v-else>Nothing more to load</span>
</button>
</div>
</template>
What happens when an infinite query needs to be refetched?
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.
Refetch page
If you only want to actively refetch a subset of all pages, you can pass the refetchPage
function to refetch
returned from useInfiniteQuery
as shown below :
function useProjectsInfiniteQuery() {
return useInfiniteQuery("projects", fetchProjects, {
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
});
}
const { refetch } = useProjectsInfiniteQuery();
// only refetch the first page
refetch({ 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[]) => boolean
The function above is executed for each page, and only pages where this function returns true
will be refetched.
What if I need to pass custom information to my query function?
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:
const fetchProjects = ({ pageParam = 0 }) =>
fetch("/api/projects?cursor=" + pageParam);
function useProjectsInfiniteQuery() {
return useInfiniteQuery("projects", fetchProjects, {
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
});
}
const { fetchNextPage } = useProjectsInfiniteQuery();
// Pass your own page param
const skipToCursor50 = () => fetchNextPage({ pageParam: 50 });
What if I want to implement a bi-directional infinite list?
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,
});
What if I want to show the pages in reversed order?
Sometimes you may want to show the pages in reversed order. If this is case, you can use the select
option:
function useProjectsInfiniteQuery() {
return useInfiniteQuery("projects", fetchProjects, {
select: (data) => ({
pages: [...data.pages].reverse(),
pageParams: [...data.pageParams].reverse(),
}),
});
}
What if I want to manually update the infinite query?
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,
}));
TIP
Make sure to keep the same data structure of pages
and pageParams
!