无限查询
渲染可增量式“加载更多”数据到现有数据集上或实现“无限滚动”的列表,也是一种非常常见的 UI 模式。TanStack Query 支持一种适用于此类列表的实用版 useQuery,称为 useInfiniteQuery。
使用 useInfiniteQuery 时,你会注意到以下几点差异:
data现在是一个包含无限查询数据的对象:data.pages数组,包含已拉取的页面data.pageParams数组,包含用于拉取各页面的页面参数- 现在可使用
fetchNextPage和fetchPreviousPage函数(其中fetchNextPage为必需项) - 现在可使用(且必需)
initialPageParam选项来指定初始页面参数 getNextPageParam和getPreviousPageParam选项可用于判断是否还有更多数据可加载,以及获取加载所需的信息。该信息将作为额外参数传入查询函数- 现在可使用
hasNextPage布尔值,当getNextPageParam返回值不为null或undefined时,其值为true - 现在可使用
hasPreviousPage布尔值,当getPreviousPageParam返回值不为null或undefined时,其值为true - 现在可使用
isFetchingNextPage和isFetchingPreviousPage布尔值,以区分后台刷新状态与“加载更多”状态
注意:
initialData或placeholderData选项需符合具有data.pages和data.pageParams属性的对象结构。
示例
假设我们有一个 API,它根据 cursor 索引每次返回 projects 的 3 个页面,并附带一个可用于获取下一组项目的游标:
1fetch('/api/projects?cursor=0')2// { data: [...], nextCursor: 3}3fetch('/api/projects?cursor=3')4// { data: [...], nextCursor: 6}5fetch('/api/projects?cursor=6')6// { data: [...], nextCursor: 9}7fetch('/api/projects?cursor=9')8// { data: [...] }基于此信息,我们可通过以下方式构建“加载更多” UI:
- 默认等待
useInfiniteQuery请求第一组数据 - 在
getNextPageParam中返回下一次查询所需的信息 - 调用
fetchNextPage函数
1import { Switch, Match, For, Show } from 'solid-js'2import { useInfiniteQuery } from '@tanstack/solid-query'3 4function Projects() {5 const fetchProjects = async ({ pageParam }) => {6 const res = await fetch('/api/projects?cursor=' + pageParam)7 return res.json()8 }9 10 const projectsQuery = useInfiniteQuery(() => ({11 queryKey: ['projects'],12 queryFn: fetchProjects,13 initialPageParam: 0,14 getNextPageParam: (lastPage, pages) => lastPage.nextCursor,15 }))16 17 return (18 <Switch>19 <Match when={projectsQuery.isPending}>20 <p>Loading...</p>21 </Match>22 <Match when={projectsQuery.isError}>23 <p>Error: {projectsQuery.error.message}</p>24 </Match>25 <Match when={projectsQuery.isSuccess}>26 <For each={projectsQuery.data.pages}>27 {(group) => (28 <For each={group.data}>{(project) => <p>{project.name}</p>}</For>29 )}30 </For>31 <div>32 <button33 onClick={() => projectsQuery.fetchNextPage()}34 disabled={!projectsQuery.hasNextPage || projectsQuery.isFetching}35 >36 {projectsQuery.isFetchingNextPage37 ? 'Loading more...'38 : projectsQuery.hasNextPage39 ? 'Load More'40 : 'Nothing more to load'}41 </button>42 </div>43 <Show44 when={projectsQuery.isFetching && !projectsQuery.isFetchingNextPage}45 >46 <div>Fetching...</div>47 </Show>48 </Match>49 </Switch>50 )51}必须理解的是,在已有拉取正在进行时调用 fetchNextPage,存在覆盖后台正在进行的数据刷新的风险。当渲染列表并同时触发 fetchNextPage 时,这一情况尤为关键。
请记住,一个 InfiniteQuery 只能存在单个正在进行的拉取。所有页面共享一个缓存条目;若尝试同时发起两次拉取,可能导致数据被覆盖。
如需启用并发拉取,可在 fetchNextPage 中使用 { cancelRefetch: false } 选项(默认值:true)。
为确保查询过程无缝且无冲突,强烈建议验证查询是否处于 isFetching 状态,尤其当用户无法直接控制该调用时。
1<List2 onEndReached={() =>3 projectsQuery.hasNextPage &&4 !projectsQuery.isFetching &&5 projectsQuery.fetchNextPage()6 }7/>当无限查询需要重新拉取时会发生什么?
当无限查询变为 stale 并需要重新拉取时,各页面将按顺序从第一个开始依次拉取 sequentially。这确保了即使底层数据被修改,我们也不会使用过期游标,从而避免重复或遗漏记录。如果无限查询的结果曾从 queryCache 中移除,则分页将重置为初始状态,仅请求初始页面。
如果我想实现双向无限列表怎么办?
可通过使用 getPreviousPageParam、fetchPreviousPage、hasPreviousPage 和 isFetchingPreviousPage 属性及函数来实现双向列表。
1useInfiniteQuery(() => ({2 queryKey: ['projects'],3 queryFn: fetchProjects,4 initialPageParam: 0,5 getNextPageParam: (lastPage, pages) => lastPage.nextCursor,6 getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,7}))如果我想以倒序显示页面怎么办?
有时你可能希望以倒序显示页面。若如此,可使用 select 选项:
1useInfiniteQuery(() => ({2 queryKey: ['projects'],3 queryFn: fetchProjects,4 select: (data) => ({5 pages: [...data.pages].reverse(),6 pageParams: [...data.pageParams].reverse(),7 }),8}))如果我想手动更新无限查询怎么办?
手动移除第一页:
1queryClient.setQueryData(['projects'], (data) => ({2 pages: data.pages.slice(1),3 pageParams: data.pageParams.slice(1),4}))手动从某一页中移除单个值:
1const newPagesArray =2 oldPagesArray?.pages.map((page) =>3 page.filter((val) => val.id !== updatedId),4 ) ?? []5 6queryClient.setQueryData(['projects'], (data) => ({7 pages: newPagesArray,8 pageParams: data.pageParams,9}))仅保留第一页:
1queryClient.setQueryData(['projects'], (data) => ({2 pages: data.pages.slice(0, 1),3 pageParams: data.pageParams.slice(0, 1),4}))务必始终维持页面和 pageParams 的相同数据结构!
如果我想限制页面数量怎么办?
在某些用例中,你可能希望限制查询数据中存储的页面数量,以提升性能和用户体验:
- 当用户可加载大量页面时(内存占用)
- 当你需要重新拉取包含数十页的无限查询时(网络开销:所有页面将被顺序拉取)
解决方案是使用“受限无限查询”。这可通过结合使用 maxPages 选项与 getNextPageParam 和 getPreviousPageParam 实现,从而支持在两个方向上按需拉取页面。
在以下示例中,查询数据的 pages 数组仅保留 3 个页面。若需重新拉取,也仅会顺序重新拉取这 3 个页面。
1useInfiniteQuery(() => ({2 queryKey: ['projects'],3 queryFn: fetchProjects,4 initialPageParam: 0,5 getNextPageParam: (lastPage, pages) => lastPage.nextCursor,6 getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,7 maxPages: 3,8}))如果我的 API 不返回游标怎么办?
如果你的 API 不返回游标,可将 pageParam 用作游标。由于 getNextPageParam 和 getPreviousPageParam 同样接收当前页面的 pageParam,你可以利用它计算出下一个 / 上一个页面参数。
1return useInfiniteQuery(() => ({2 queryKey: ['projects'],3 queryFn: fetchProjects,4 initialPageParam: 0,5 getNextPageParam: (lastPage, allPages, lastPageParam) => {6 if (lastPage.length === 0) {7 return undefined8 }9 return lastPageParam + 110 },11 getPreviousPageParam: (firstPage, allPages, firstPageParam) => {12 if (firstPageParam <= 1) {13 return undefined14 }15 return firstPageParam - 116 },17}))延伸阅读
要更深入地了解无限查询在底层的工作机制,请参阅文章 无限查询的工作原理。