渲染可以累加地“加载更多”数据到现有数据集或“无限滚动”的列表也是一种非常常见的 UI 模式。TanStack Query 支持一个有用的 `useQuery` 版本,称为 `useInfiniteQuery`,用于查询这类列表。
使用 useInfiniteQuery 时,你会注意到一些不同之处:
注意:`initialData` 或 `placeholderData` 选项需要符合相同的结构,即一个包含 `data.pages` 和 `data.pageParams` 属性的对象。
假设我们有一个 API,它每次返回 3 页的 `projects`,基于一个 `cursor` 索引,以及一个可以用来获取下一组项目的游标。
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: [...] }
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: [...] }
有了这些信息,我们可以通过以下方式创建一个“加载更多”UI:
import { useInfiniteQuery } from '@tanstack/solid-query'
function Projects() {
const fetchProjects = async ({ pageParam }) => {
const res = await fetch('/api/projects?cursor=' + pageParam)
return res.json()
}
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
status,
} = useInfiniteQuery(() => {
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})
return status === 'pending' ? (
<p>Loading...</p>
) : status === 'error' ? (
<p>Error: {error.message}</p>
) : (
<>
{data.pages.map((group, i) => (
<React.Fragment key={i}>
{group.data.map((project) => (
<p key={project.id}>{project.name}</p>
))}
</React.Fragment>
))}
<div>
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetching}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'Nothing more to load'}
</button>
</div>
<div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div>
</>
)
}
import { useInfiniteQuery } from '@tanstack/solid-query'
function Projects() {
const fetchProjects = async ({ pageParam }) => {
const res = await fetch('/api/projects?cursor=' + pageParam)
return res.json()
}
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
status,
} = useInfiniteQuery(() => {
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})
return status === 'pending' ? (
<p>Loading...</p>
) : status === 'error' ? (
<p>Error: {error.message}</p>
) : (
<>
{data.pages.map((group, i) => (
<React.Fragment key={i}>
{group.data.map((project) => (
<p key={project.id}>{project.name}</p>
))}
</React.Fragment>
))}
<div>
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetching}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'Nothing more to load'}
</button>
</div>
<div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div>
</>
)
}
理解在使用 `fetchNextPage` 时,如果正在进行其他获取操作,可能会覆盖后台发生的数据刷新,这一点非常重要。当渲染列表并同时触发 `fetchNextPage` 时,这种情况尤其关键。
请记住,InfiniteQuery 只能有一个正在进行的 fetch。所有页面共享一个缓存条目,同时尝试两次 fetch 可能会导致数据覆盖。
如果您打算启用同时获取,可以在 `fetchNextPage` 中使用 `{ cancelRefetch: false }` 选项(默认为 true)。
为了确保无冲突的顺畅查询过程,强烈建议验证查询是否处于 `isFetching` 状态,尤其是在用户不直接控制该调用时。
<List onEndReached={() => hasNextPage && !isFetching && fetchNextPage()} />
<List onEndReached={() => hasNextPage && !isFetching && fetchNextPage()} />
当一个无限查询变得 `stale`(过期)并需要重新获取时,每个组都会被 `sequentially`(顺序地)获取,从第一个开始。这确保了即使底层数据被修改,我们也不会使用过期的游标,从而避免重复或遗漏记录。如果无限查询的结果从 `queryCache` 中被移除,分页将重新开始,回到初始状态,只请求初始组。
通过使用 `getPreviousPageParam`、`fetchPreviousPage`、`hasPreviousPage` 和 `isFetchingPreviousPage` 属性和函数,可以实现双向列表。
useInfiniteQuery(() => {
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
})
useInfiniteQuery(() => {
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
})
有时你可能希望以相反的顺序显示页面。如果是这种情况,你可以使用 select 选项。
useInfiniteQuery(() => {
queryKey: ['projects'],
queryFn: fetchProjects,
select: (data) => ({
pages: [...data.pages].reverse(),
pageParams: [...data.pageParams].reverse(),
}),
})
useInfiniteQuery(() => {
queryKey: ['projects'],
queryFn: fetchProjects,
select: (data) => ({
pages: [...data.pages].reverse(),
pageParams: [...data.pageParams].reverse(),
}),
})
queryClient.setQueryData(['projects'], (data) => ({
pages: data.pages.slice(1),
pageParams: data.pageParams.slice(1),
}))
queryClient.setQueryData(['projects'], (data) => ({
pages: data.pages.slice(1),
pageParams: data.pageParams.slice(1),
}))
const newPagesArray =
oldPagesArray?.pages.map((page) =>
page.filter((val) => val.id !== updatedId),
) ?? []
queryClient.setQueryData(['projects'], (data) => ({
pages: newPagesArray,
pageParams: data.pageParams,
}))
const newPagesArray =
oldPagesArray?.pages.map((page) =>
page.filter((val) => val.id !== updatedId),
) ?? []
queryClient.setQueryData(['projects'], (data) => ({
pages: newPagesArray,
pageParams: data.pageParams,
}))
queryClient.setQueryData(['projects'], (data) => ({
pages: data.pages.slice(0, 1),
pageParams: data.pageParams.slice(0, 1),
}))
queryClient.setQueryData(['projects'], (data) => ({
pages: data.pages.slice(0, 1),
pageParams: data.pageParams.slice(0, 1),
}))
请务必始终保持页面和页面参数的相同数据结构!
在某些用例中,你可能希望限制查询数据中存储的页面数量,以提高性能和用户体验
解决方案是使用“限制性无限查询”。这可以通过使用 `maxPages` 选项,并结合 `getNextPageParam` 和 `getPreviousPageParam` 来实现,从而允许在需要时双向获取页面。
在以下示例中,查询数据页面数组中只保留 3 个页面。如果需要重新获取,将只按顺序重新获取 3 个页面。
useInfiniteQuery(() => {
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
maxPages: 3,
})
useInfiniteQuery(() => {
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
maxPages: 3,
})
如果您的 API 不返回游标,您可以使用 `pageParam` 作为游标。因为 `getNextPageParam` 和 `getPreviousPageParam` 也会获取当前页的 `pageParam`,所以您可以使用它来计算下一个/上一个页面的参数。
return useInfiniteQuery(() => {
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam) => {
if (lastPage.length === 0) {
return undefined
}
return lastPageParam + 1
},
getPreviousPageParam: (firstPage, allPages, firstPageParam) => {
if (firstPageParam <= 1) {
return undefined
}
return firstPageParam - 1
},
})
return useInfiniteQuery(() => {
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam) => {
if (lastPage.length === 0) {
return undefined
}
return lastPageParam + 1
},
getPreviousPageParam: (firstPage, allPages, firstPageParam) => {
if (firstPageParam <= 1) {
return undefined
}
return firstPageParam - 1
},
})
要更好地理解无限查询的底层工作原理,请阅读社区资源中的 无限查询是如何工作的。