使用useInfiniteQuery和封装的useInfiniteScrollHooks做一个无限feed流
在这篇文章中,我们将使用 React Query 的 useInfiniteQuery
hook 和一个自定义的 useInfiniteScroll
hook 来创建一个无限滚动的 feed 流。
使用 useInfiniteQuery
获取数据
首先,我们使用 useInfiniteQuery
来获取 feed 数据。这个 hook 接受一个查询键和一个获取数据的异步函数作为参数,并返回一个对象,该对象包含了我们需要的所有状态和函数。
在这个例子中,我们使用 getFeed
函数来获取数据,该函数接受一个页码作为参数。我们还提供了一个 getNextPageParam
函数来确定下一页的参数。
tsx
const {
data: FeedData = { pages: [], pageParams: [] },
isLoading: FeedIsLoading,
isError: FeedIsError,
fetchNextPage,
isFetchingNextPage,
hasNextPage,
} = useInfiniteQuery(['getFeed'], ({ pageParam = 0 }) => getFeed(pageParam), {
getNextPageParam: (lastPage, pages) => {
if (lastPage.length === 0) return undefined;
return pages.length + 1;
},
});
当前后端给我们的接口是这样的,我们将它封装成getFeed
ts
// 获取feed
export async function getFeed(pageParam: number, per_page: number = 5) {
return axios.get({
url: 'feed',
params: {
page: pageParam, // 第几页
per_page, // 一次获取多少页
},
});
}
使用自定义的 useInfiniteScroll
Hook
然后,我们使用自定义的 useInfiniteScroll
hook 来实现无限滚动。这个 hook 接受当前是否正在获取下一页、是否有下一页和获取下一页的函数作为参数,并返回一个可以被赋予给元素的 ref。
当这个 ref 被赋予给的元素出现在视口中时,就会调用获取下一页的函数。
tsx
const lastElementRef = useInfiniteScroll(
isFetchingNextPage,
hasNextPage,
fetchNextPage
);
再解释一下这个hooks,这个 Hook 接受三个参数:
isFetching
:一个布尔值,表示是否正在获取下一页的数据。hasNextPage
:一个布尔值,表示是否还有下一页的数据。fetchNextPage
:一个函数,当需要获取下一页的数据时会被调用。
这个 Hook会 返回一个函数,这个函数接受一个 HTMLDivElement
也就是一个ref对象并对其进行操作。当这个元素出现在视口中时,就会调用 fetchNextPage
函数来获取下一页的数据。
它 的工作原理是使用 IntersectionObserver
API 来观察一个元素(在这个例子中是传入的 node
)。当这个元素出现在视口中时,IntersectionObserver
会调用它的回调函数。在这个回调函数中,我们检查是否正在获取数据(通过 isFetching
)和是否还有下一页(通过 hasNextPage
)。如果不在获取数据并且还有下一页,就调用 fetchNextPage
函数。
最重要的就是这个IntersectionObserver的方法
tsx
import { RefObject, useRef, useCallback } from 'react';
type FetchNextPageFunction = () => void;
export function useInfiniteScroll(
isFetching: boolean,
hasNextPage: boolean | undefined,
fetchNextPage: FetchNextPageFunction
): (node: HTMLDivElement) => void {
const observer = useRef<IntersectionObserver | null>(null);
const lastElementRef = useCallback(
(node: HTMLDivElement) => {
if (isFetching) return;
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasNextPage) {
fetchNextPage();
}
});
if (node) observer.current.observe(node);
},
[isFetching, hasNextPage]
);
return lastElementRef;
}
渲染 Feed
最后,我们渲染 feed。对于每一页,我们都渲染一个 div,并在其中渲染每个 feed item。我们使用 Card
组件来渲染每个 item。
当有下一页时,我们在列表底部添加一个 "Loading..." 的 div,并将其 ref 设置为 lastElementRef
。当这个 div 出现在视口中时,就会触发获取下一页的函数。
在使用FeedData要注意它的是一个{ pages: [], pageParams: [] }对象,并且pages是一个二维数组,它是全部的数据分组的集合。每次fetchNextPage就会使group+1,这个新的group的数组就是获取到的下一页数组
tsx
return (
<>
{FeedData.pages &&
FeedData.pages.map((group, i) => (
<div key={i}>
{group.map((feedItem: any) => (
<Card key={feedItem.uid} item={feedItem} /> //Card 是自己的组件
))}
</div>
))}
{hasNextPage && (
<div ref={lastElementRef} className=' text-center p-4'>
Loading...
</div>
)}
</>
);
通过这种方式,我们就可以创建一个无限滚动的 feed 流了。