useInfiniteQuery() API
看名字就能猜出来,useInfiniteQuery() 是专门用来应付无限查询场景的。不仅如此,useInfiniteQuery() API 能力也是基于 useQuery() 的。
之前的文章中我们介绍了 useQuery() 的核心 API,为了找回印象,我们在此贴出来:
import { useQuery } from 'react-query'
const {
data,
error,
isError,
isFetching,
isLoading,
isRefetching,
isSuccess,
refetch,
} = useQuery(queryKey, queryFn?, {
enabled,
onError,
onSuccess,
refetchOnWindowFocus,
retry,
staleTime,
})
如果我们把这些 API 简化如下:
const {
...result,
} = useQuery(queryKey, queryFn?, {
...options,
})
useInfiniteQuery() 其实就是在 useQuery() 基础之上增添一些无限查询场景的参数:
const {
fetchNextPage,
fetchPreviousPage,
hasNextPage,
hasPreviousPage,
isFetchingNextPage,
isFetchingPreviousPage,
...result
} = useInfiniteQuery(queryKey, ({ pageParam = 1 }) => fetchPage(pageParam), {
...options,
getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor,
})
如你所见,增加的 API 其实就是跟上一页/下一页查询动作相关的参数,相比较于自己组装 的分页查询能力的 useQuery(),useInfiniteQuery() 需要配置上一页/下一页的参数获取函数,并提供了相应的查询调用能力,更加自动化和便捷。
当然,增加的不只是参数,还有 2 处:
一个是 queryFn 参数的入参,多了一个名为 pageParam 的参数。
pageParam 表示当前页数。这个值是每次 useInfiniteQuery() 调用时,通过 getNextPageParam()/getPreviousPageParam() 返回值自动获取并传入 queryFn 的。
第二个还有返回值的数据结构,即 data。
const { data } = useInfiniteQuery()
原来 data 就是表示内部请求方法的返回值。而 useInfiniteQuery() 的返回 data 因为要包含多页数据(展示旧数据时,还要持有旧数据),因此 data 变更为:
data: { pages: TData[], pageParams: unknown[] }
pages 很好理解,就是用来承载过程中请求到的多页数据;pageParams 则是每个页面当时在做数据获取时使用的查询参数。
简单一例
当然语言上说得再多,也是苍白无力的,实践出真知。这里我们就举一个简单的例子说明 useInfiniteQuery() 的使用。
首先,我们先创建一个获取数据的请求函数(使用 Fetch API)。
const getPosts = async (pageParam) => {
return fetch(`https://jsonplaceholder.typicode.com/posts?_page=${pageParam.page}&_limit=${pageParam.size}`).then(res => res.json())
}
接着,使用 useInfiniteQuery() 请求数据:
function Example() {
const {
isLoading,
isError,
error,
data,
} = useInfiniteQuery(
'posts',
({ pageParam }) => getPosts(pageParam),
{
getNextPageParam: (lastPage, allPages) => ({ page: allPages.length + 1, size: 6 }),
refetchOnWindowFocus: false, // Prevent refetching on window focus
}
)
// ...
}
增加下加载中或出现异常时的处理逻辑。
function Example() {
// ...
if (isLoading) {
return <div>Loading...</div>;
}
if (isError) {
return <div>Error: {error.message}</div>
}
// ...
}
最后渲染分页数据。
function Example() {
// ...
return (
<div>
<ol>
{/* (1) */}
{data.pages.map((page) => (
{page.map((post) => (
<li key={post.id}>{post.title}</li>
))}
))}
</ol>
{/* (2) */}
<button onClick={() => fetchNextPage()}>More</button>
</div>
)
}
- 遍历 data.pages 中所有页面数据,渲染出来
- 使用 fetchNextPage() 函数加载更多(实际上即“下一页”)数据
浏览器访问,不幸运是,报错了。
图片
没关系,点击查看报错处。
图片
发现是是由于首次请求接口时,pageParam 参数是 undefined,这就需要我们给 pageParam 一个默认值。
- const getPosts = async (pageParam) => {
+ const getPosts = async (pageParam = { page: 1, size: 6 }) => {
return fetch(`https://jsonplaceholder.typicode.com/posts?_page=${pageParam.page}&_limit=${pageParam.size}`).then(res => res.json())
}
再次访问页面,发现我们成功请求到了第一页数据。
图片
接下来点击“More”按钮,发现发起了到第二页数据的请求。
图片
图片
这里逻辑是先调用 getNextPageParam() 函数获得请求参数。getNextPageParam() 函数的类型声明如下:
getNextPageParam: (lastPage, allPages) => unknown | undefined
第一个参数就是上一页数据,第二个参数即到目前为止请求到的所有数据。对照本例,当我们第一次点击“More”按钮时,返回的值为 { page:2, size:6 }。然后,再发起 getPosts 调用并这个参数,于是我们便发起了第二页的数据请求,并最终得到了数据。
下面,我们再给“More”按钮增加一个加载状态——我们可以使用通用的 isFetching 状态,但这里我们使用更具体的 isFetchingNextPage 状态。
function Example() {
const {
isLoading,
+ isFetchingNextPage,
isError,
error,
data,
} = useInfiniteQuery()
添加给按钮。
- <button notallow={() => fetchNextPage()}>More</button>
+ <button disabled={isFetchingNextPage} notallow={() => fetchNextPage()}>More</button>
查看效果。
图片
这样,我们就能在请求下一页数据的时候禁用按钮点击,避免用户误触。
再来一例
以上案例中,我们是通过 getNextPageParam() 所提供的第二个参数 allPages 来获得下一页页码的。
getNextPageParam: (lastPage, allPages) => ({ page: allPages.length + 1, size: 6 })
这是因为返回的第一页数据总并未包含下一页信息。
图片
我们再稍微修改下
const getPosts = async (pageParam = { page: 1, size: 6 }) => {
return fetch(`https://jsonplaceholder.typicode.com/posts?_page=${pageParam.page}&_limit=${pageParam.size}`).then(res => {
const total = res.headers.get('X-Total-Count')
return res.json().then(data => {
return {
total,
data,
hasMore: pageParam.page * pageParam.size < total
}
})
})
}
我们将返回数据放在 data 属性中,并返回数据总数(total)以及是否还有数据(hasMore)。
有了这些信息,我们就可以有了更加细腻的交互了。
下面,修改渲染逻辑。
function Example() {
// ...
return (
<div>
<ol>
{data.pages.map((page) => (
<>
{page.data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</>
))}
</ol>
<p>总共 <strong>{data.pages[0].total}</strong> 条数据</p>
{
data.pages[data.pages.length - 1].hasMore ? (
<button disabled={isFetchingNextPage} onClick={() => fetchNextPage()}>More</button>
) : <span>--- 我是有底线的 ---</span>
}
</div>
)
}
查看初始加载效果。
图片
查看最终效果。
图片
不过,按钮显隐逻辑还是有些冗余。
{
data.pages[data.pages.length - 1].hasMore ? (
<button disabled={isFetchingNextPage} onClick={() => fetchNextPage()}>More</button>
) : <span>--- 我是有底线的 ---</span>
}
这一点 useInfiniteQuery 也帮我们想到了,便提供了一个 hasNextPage 供我们使用。
{
- data.pages[data.pages.length - 1].hasMore ? (
+ hasNextPage ? (
<button disabled={isFetchingNextPage} notallow={() => fetchNextPage()}>More</button>
) : <span>--- 我是有底线的 ---</span>
}
hasNextPage 为 true 表示有下一页数据,为 false 表示没有数据了。
不过 hasNextPage 的值是受到 getNextPageParam() 返回值影响的——当 getNextPageParam() 返回 undefined 时,hasNextPage 值为 false,否则为 true。
因此,我们修改下 getNextPageParam() 的判断逻辑。
getNextPageParam: (lastPage, allPages) => {
return lastPage.hasMore
? { page: allPages.length + 1, size: 50 }
: undefined
},
如此,我们再来查看下效果。
图片
发现没有数据的时候,同样也不会展示“More”按钮了。大功告成!
下拉拉取新数据
当然,以上通过点击“More”按钮加载新数据的方式还是太麻烦了一些。如果我们在网页下拉到底部的时候自动获取数据不是更好吗?
下面就来实现一下。
我们将借助 IntersectionObserver API[4] + 底部一个 .loadMore 元素来实现。
首先,将 “More” 按钮部分的代码替换成:
<div className="loadMore" style={{ height: '30px', lineHeight: '30px' }} ref={loadMoreRef}>
{
isFetchingNextPage
? <span>Loading...</span>
: <span>--- 我是有底线的 ---</span>
}
</div>
注意,我们同时通过 loadMoreRef 拿到 DOM 元素。
然后,我们监听 .loadMore 元素,一旦出现在屏幕中,就调用 fetchNextPage() 获取下一页数据。
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasNextPage) {
fetchNextPage();
}
});
if (loadMoreRef.current) {
observer.observe(loadMoreRef.current);
}
return () => observer.disconnect();
}, [hasNextPage, fetchNextPage]);
来查看效果。
图片
完美。
最后,再把完整代码贴出来,方便大家学习。
import { useEffect, useRef } from 'react'
import { QueryClient, QueryClientProvider, useInfiniteQuery } from 'react-query'
// Create a client
const queryClient = new QueryClient()
export default function App() {
return (
// Provide the client to your App
<QueryClientProvider client={queryClient}>
<Example />
</QueryClientProvider>
)
}
const getPosts = async (pageParam = { page: 1, size: 25 }) => {
return fetch(`https://jsonplaceholder.typicode.com/posts?_page=${pageParam.page}&_limit=${pageParam.size}`).then(res => {
const total = res.headers.get('X-Total-Count')
return res.json().then(data => {
return {
total,
data,
hasMore: pageParam.page * pageParam.size < total
}
})
})
}
function Example() {
const {
isLoading,
isFetchingNextPage,
hasNextPage,
isError,
error,
data,
fetchNextPage
} = useInfiniteQuery(
'posts',
({ pageParam }) => getPosts(pageParam),
{
getNextPageParam: (lastPage, allPages) => {
return lastPage.hasMore ? { page: allPages.length + 1, size: 25 } : undefined
},
refetchOnWindowFocus: false, // Prevent refetching on window focus
}
)
const loadMoreRef = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasNextPage) {
fetchNextPage();
}
});
if (loadMoreRef.current) {
observer.observe(loadMoreRef.current);
}
return () => observer.disconnect();
}, [hasNextPage, fetchNextPage]);
if (isLoading) {
return <div>Loading...</div>;
}
if (isError) {
return <div>Error: {error.message}</div>
}
return (
<div>
<p>总共 <strong>{data.pages[0].total}</strong> 条数据</p>
<ol>
{data.pages.map((page) => (
<>
{page.data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</>
))}
</ol>
<div className="loadMore" style={{ height: '30px', lineHeight: '30px' }} ref={loadMoreRef}>
{
isFetchingNextPage ? <span>Loading...</span> : <span>--- 我是有底线的 ---</span>
}
</div>
</div>
)
}
总结
本文我们讲述了 React Query 中用于无限查询 API useInfiniteQuery() 的使用。
通过循序渐进的 3 个案例,最终实现了一个下拉到底后自动新数据的交互效果,还是比较好实现的。
当然,本文只是以“下一页”举例,“上一页”与此同理。
希望本位讲述的内容能够对你的工作有所帮助。感谢阅读,再见。
参考资料
[1]React Query 是做什么的?: https://juejin.cn/post/7378015213348257855
[2]一个数据获竟然被 React Query 玩出这么多花样来!: https://juejin.cn/post/7380342160581918731
[3]React Query 的 useQuery 竟也内置了分页查询支持!: https://juejin.cn/post/7380569775686746151
[4]IntersectionObserver API: https://ruanyifeng.com/blog/2016/11/intersectionobserver_api.html