react-query手把手教程15-无限加载数据
该系列其他文章可以点击查看专栏👉🏻👉🏻react-query手把手教程系列
场景
在前面的章节章,讲述了如何使用react-query
进行更好的分页操作。但是现在越来越多的网站已经采用无限流模式,比如你在花瓣网搜索“大橙子”,把页面滑到底部,并没有展示出分页器,而是直接为你展示出了新的搜索结果。
接下来会介绍react-query专门为解决该种情况下的方案useInfiniteQuery
。
实践
useInfiniteQuery
useInfiniteQuery
与useQuery
在大多数的设计上是一致的,包括都可以传入查询键和查询函数,并且会返回后端数据(data
), 是否第一次获取数据中(isLoading
), 是否错误(isError
), 是否正在加载数据中(isFetching
)等,但是依旧还是有一些区别的地方。
首先useInfiniteQuery
钩子的查询函数将会接收到一个pageParam
的参数,该参数为当前的翻页状态,你可以通过这个参数,获取相关的数据。这里还是以拉取github issue列表接口为例:
const fetchInfiniteIssues = async ({queryKey, pageParam = 1}) => {
const [issues, org, repo] = queryKey
const response = await fetch(
`https://api.github.com/repos/${org}/${repo}/issues?page=${pageParam}`
)
const json = await response.json()
return json
}
但是这个pageParam
是从哪里来的呢?这就需要介绍一下useInfiniteQuery
钩子的不同之处了,这个钩子需要传递一个getNextPageParam
配置项,并且该配置项需要需要传入一个函数,pageParam
就是getNextPageParam
传入函数的返回值。
如果获取到的数据中,已经没有下一页的数据了,直接返回undefined
即可,如果有数据那么直接返回当前第n
页+1的结果即可:
const issuesInfiniteQuery = useInfiniteQuery(
['issues', org, repo],
fetchInfiniteIssues,
{
getNextPageParam: (lastPage, pages) => {
if (lastPage.length === 0) {
return;
}
return pages.length + 1;
}
}
)
如果页面采用游标进行分页,此时需要返回下一页的游标即可,伪代码如下:
const issuesInfiniteQuery = useInfiniteQuery( ['issues', org, repo], fetchInfiniteIssues, { getNextPageParam: (lastPage, pages) => { return lastPage.nextCursor; } } )
该钩子在返回值的结构与useQuery
也有些许不同,数据将会被挂载在pages
下。
渲染github issue列表的伪代码如下:
const InfiniteIssues = () => {
const issuesInfiniteQuery = useInfiniteQuery(
['issues', org, repo],
fetchInfiniteIssues,
{
getNextPageParam: (lastPage, pages) => {
if (lastPage.length === 0) {
return;
}
return pages.length + 1;
}
}
)
return (
<div>
{issuesInfiniteQuery.data.pages.map((page, index) => (
<Fragment key={index}>
{page.map(issue => (
<Issue key={issue.id} issue={issue} />
))}
</Fragment>
))}
</div>
)
}
目前已经搞定了展示以及参数问题,接下来就是怎么触发获取下一页的逻辑,react-query提供了fetchNextPage
属性来支持该需求,一旦调用fetchNextPage
方法后,getNextPageParam
方法将会被触发,来确定下一个页面的参数是什么,并调用查询函数,最终将结果添加到现有的页面列表中。
加载更多按钮
你可以通过点击加载更多按钮来实现该需求,这是最简单的一种实现方式,伪代码如下:
const InfiniteIssues = () => {
const issuesInfiniteQuery = useInfiniteQuery(
// ...
);
return (
<div>
{issuesInfiniteQuery.data.pages.map((page, index) => (
<Fragment key={index}>
{page.map(issue => (
<Issue key={issue.id} issue={issue} />
))}
</Fragment>
))}
<button onClick={
() => issuesInfiniteQuery.fetchNextPage()
}
disabled={
!issuesInfiniteQuery.hasNextPage ||
issuesInfiniteQuery.isFetchingNextPage
}
>
{issuesInfiniteQuery.isFetchingNextPage ?
"正在加载下一页数据..." :
"点击加载更多数据"}
</button>
</div>
)
}
当用户点击了按钮后,将会调用fetchNextPage
属性方法加载下一页,你可以通过isFetchingNextPage
属性来知道当前是否在加载状态。
监听页面是否滚动到底部加载下一页数据
先实现一个react hook来监听当前页面是否已经滑动到底部:
function useScrollToBottomAction(container, callback, offset = 0) {
const callbackRef = useRef(callback);
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
useEffect(() => {
const handleScroll = () => {
let scrollContainer =
container === document ? document.scrollingElement : container;
if (
scrollContainer.scrollTop + scrollContainer.clientHeight >=
scrollContainer.scrollHeight - offset
) {
callbackRef.current();
}
};
container.addEventListener("scroll", handleScroll, {passive:true});
return () => {
container.removeEventListener("scroll", handleScroll);
};
}, [container, offset]);
}
使用该钩子你需要传入以下三个参数
container
: 需要监听滚动的dom元素callback
:监听滚动到底部后回调的方法offset
: 如果你需要在离底部一定距离时就触发触底事件,你需要在这里设置相关的偏移量
编写好监听页面触底的钩子后,就需要实际使用它来实现相关功能了,伪代码如下:
const InfiniteIssues = () => {
const issuesInfiniteQuery = useInfiniteQuery(
// ...
);
useScrollToBottomAction(
document,
() => {
// ①
if (issuesInfiniteQuery.isFetchingNextPage) return;
issuesInfiniteQuery.fetchNextPage();
},
500
);
return (
<div>
{issuesInfiniteQuery.data.pages.map((page, index) => (
<Fragment key={index}>
{page.map(issue => (
<Issue key={issue.id} issue={issue} />
))}
</Fragment>
))}
{issuesInfiniteQuery.isFetchingNextPage && (
<div>正在加载下一页数据...</div>
)}
</div>
)
①:为了防止代码在加载过程中被多次触发,需要监听isFetchingNextPage
属性为true
时,不触发加载下一页的功能
重新获取数据
如果用户有重新获取数据的需求,react-query的策略将会是直接从第一页开始,一直获取到当前最近的页面。
使用这种更新策略的原因是因为不从初始的页面获取数据,假如是使用游标获取下一页的数据,一旦后端有新增和删除操作,下一页的游标就会发生变化,按照之前的游标重新刷新下一页就会出现问题。
双向无限查询
通常的分页查询中,后端都会告诉前端上一页以及下一页获取的参数,比如游标或者偏移量。
此时如果用户是在非第一页打开的无限流数据,比如第三页,此时你可以向useInfiniteQuery
钩子传入getPreviousPageParam
参数,通过fetchPreviousPage
属性触发加载上一页的功能,同时通过isFetchingPreviousPage
属性判断当前加载上一页接口是否正在请求数据,通过hasPreviousPage
属性判断是否有上一页(其实是基于getPreviousPageParam
配置项返回的参数)
github的伪代码如下:
const InfiniteIssues = () => {
const issuesInfiniteQuery = useInfiniteQuery(
["issues", org, repo, {startingPage: 3}],
fetchInfiniteIssues,
{
getNextPageParam: (lastPage, pages) => {
if (lastPage.issueList.length === 0) {
return;
}
return lastPage.pageParam + 1;
},
getPreviousPageParam: (firstPage, pages) => {
if (firstPage.pageParam <= 1) {
return;
}
return firstPage.pageParam - 1;
}
}
);
return (
<div>
<button onClick={
() => issuesInfiniteQuery.fetchPreviousPage()
}
disabled={
!issuesInfiniteQuery.hasPreviousPage ||
issuesInfiniteQuery.isFetchingPreviousPage}
>
{issuesInfiniteQuery.isFetchingPreviousPage ?
"加载上一页数据中..." :
"点击加载更多"}
</button>
{issuesInfiniteQuery.data.pages.map((page, index) => (
<Fragment key={index}>
{page.issueList.map(issue => (
<Issue key={issue.id} issue={issue} />
))}
</Fragment>
))}
<button onClick={
() => issuesInfiniteQuery.fetchNextPage()
}
disabled={
!issuesInfiniteQuery.hasNextPage ||
issuesInfiniteQuery.isFetchingNextPage}
>
{issuesInfiniteQuery.isFetchingNextPage ?
"加载下一页数据中..." :
"点击加载更多"}
</button>
</div>
)
}
这里笔者为了方便采用的是点击按钮的方式触发加载,你也可以写一个方法,如果用户向上滚动距离顶部一段距离后,可以触发此加载逻辑。
到本文为止,react-query的基本使用方法已经介绍完毕,接下来的文章中将会介绍一些进阶用法,比如如何在graphql中使用react-query,以及react-query如何与websocket结合使用等
转载自:https://juejin.cn/post/7204345146162724920