Rendering lists that can additively "load more" data onto an existing set of data or "infinite scroll" is also a very common UI pattern. Svelte 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 undefined
.hasPreviousPage
boolean is now available and is true
if getPreviousPageParam
returns a value other than undefined
.isFetchingNextPage
and isFetchingPreviousPage
booleans are now available to distinguish between a background refresh state and a loading more stateLet'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 defaultgetNextPageParam
fetchNextPage
functionNote: It's very important you do not call
fetchNextPage
with arguments unless you want them to override thepageParam
data returned from thegetNextPageParam
function. eg. Do not do this:<button onClick={fetchNextPage} />
as this would send the onClick event to thefetchNextPage
function.
<script>import { useInfiniteQuery } from '@sveltestack/svelte-query'const fetchProjects = async ({ pageParam = 0 }) => {const { data } = await axios.get(`/projects?cursor=${pageParam}`)return data}const queryResult = useInfiniteQuery('projects', fetchProjects, {getNextPageParam: lastGroup => lastGroup.nextId || undefined,})</script>{#if $queryResult.status === 'loading'}Loading...{:else if $queryResult.status === 'error'}<span>Error: {$queryResult.error.message}</span>{:else}<div>{#each $queryResult.data.pages as page}{#each page.data as project}<p>{project.name}</p>{/each}{/each}</div><div><buttonon:click={() => $queryResult.fetchNextPage()}disabled={!$queryResult.hasNextPage || $queryResult.isFetchingNextPage}>{#if $queryResult.isFetching}Loading more...{:else if $queryResult.hasNextPage}Load More{:else}Nothing more to load{/if}</button></div>{/if}
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.
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:
<script>import { useInfiniteQuery } from '@sveltestack/svelte-query'const fetchProjects = async ({ pageParam = 0 }) => {const { data } = await axios.get(`/projects?cursor=${pageParam}`)return data}const queryResult = useInfiniteQuery('projects', fetchProjects, {getNextPageParam: lastGroup => lastGroup.nextId || undefined,})// Pass your own page paramconst skipToCursor50 = () => $queryResult.fetchNextPage({ pageParam: 50 })</script>
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),}))
Sometimes you may want to enable or disable a Query by using a svelte reactive statement. In this case, you can use the setEnabled
function:
<script>import { useInfiniteQuery } from '@sveltestack/svelte-query'export let isEnabled = false;const queryResult = useInfiniteQuery('projects', fetchProjects, {getNextPageParam: lastGroup => lastGroup.nextId || undefined,})$: queryResult.setEnabled(isEnabled)</script>{#if $queryResult.status === 'idle'}<button on:click={() => isEnabled = true}>Enable</button>{#if $queryResult.status === 'loading'}Loading...{:else if $queryResult.status === 'error'}<span>Error: {$queryResult.error.message}</span>{:else}<div>{#each $queryResult.data.pages as page}{#each page.data as project}<p>{project.name}</p>{/each}{/each}</div><div><buttonon:click={() => $queryResult.fetchNextPage()}disabled={!$queryResult.hasNextPage || $queryResult.isFetchingNextPage}>{#if $queryResult.isFetching}Loading more...{:else if $queryResult.hasNextPage}Load More{:else}Nothing more to load{/if}</button></div>{/if}
The latest TanStack news, articles, and resources, sent to your inbox.