在这篇文章中我们将谈谈 React Query 这个状态管理工具提供的一个令人惊叹的功能,即无限滚动(Infinite Scroll)。
可能你已经在每个社交媒体平台上看到了这个功能,比如 Twitter、Facebook、LinkedIn 等。在这些平台上,我们不再使用传统的分页,而是通过无限滚动来加载数据。没有上一页或下一页的按钮,数据会根据需要自动生成。但在底层,无限滚动仍然是分页的一种形式。
下面让我们看看代码吧!
首先,在我们的项目中创建 Todos 组件的文件夹:src/Todo/index.tsx
。
在其他情况下,我可能会创建一个 types.ts
文件来存放我们的类型,但这次我们只会在这个文件中使用。因此,我将在我们的组件中创建类型。此外,我将添加 MAX_POST_PAGE
常量。
// src/Todo/index.tsx
const MAX_POST_PAGE = 10;
interface TodoType {
id: number;
title: string;
}
因此,我们将有一个限制为 10 的 MAX_POST_PAGE
和我们的 Todo 类型,该类型只使用 id 和 title。
接下来让我们创建用于获取数据的函数:
// src/Todo/index.tsx
const fetchTodos = async ({ pageParam }: { pageParam: number }) => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos?_pages=${pageParam}&_limit=${MAX_POST_PAGE}`
);
const todos = (await response.json()) as TodoType[];
return todos;
};
要注意,该函数将接收 pageParam
,表示页码。我会将其作为对象接收并使用解构。
// src/Todo/index.tsx
const MAX_POST_PAGE = 10;
interface TodoType {
id: number;
title: string;
}
const fetchTodos = async ({ pageParam }: { pageParam: number }) => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos?_pages=${pageParam}&_limit=${MAX_POST_PAGE}`
);
const todos = (await response.json()) as TodoType[];
return todos;
};
export const Todo = () => {
return <></>;
};
接下来我们创建来 Todo 组件的内容。
使用 useRef
钩子创建一个观察者引用,并将 IntersectionObserver
类型作为泛型传递,如下所示:
// src/Todo/index.tsx
const observer = useRef<IntersectionObserver>();
观察者是一种设计模式,定义了对象之间的一对多依赖关系,以便当对象更改状态时,所有依赖项都会被通知并自动更新。观察者,顾名思义,将观察某个对象的状态。如果依赖项更新,正在监听(观察)的对象将被通知。
但你可能会想 🤔 为什么我要解释所有这些概念,我们将需要使用观察者来查看用户是否在页面底部,以便传递下一个页面参数时获取新数据。
所以,是的!正如我之前所说,无限滚动是一种不同类型的分页 🤯
让我们使用 React Query 的 useInfiniteQuery
钩子。它与 useQuery
非常相似:
// src/Todo/index.tsx
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetching,
isLoading
} = useInfiniteQuery({
queryKey: ["todos"],
queryFn: ({ pageParam }) => fetchTodos({ pageParam }),
getNextPageParam: (lastPage, allPages) => {
return lastPage.length ? allPages.length + 1 : undefined;
},
});
我们将解构并获取数据、错误消息、fetchNextPage
函数、hasNextPage
属性、isFetching
和 isLoading
状态。
我们将在 queryKey
中传递键值 'todos',在 queryFn
中传递 fetchTodos
函数,并在 getNextPageParam
中创建一个函数来获取下一页,增加并验证我们是否有数据。
现在让我们创建一个函数来观察用户是否到达页面底部:
// src/Todo/index.tsx
const lastElementRef = useCallback(
(node: HTMLDivElement) => {
if (isLoading) return;
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasNextPage && !isFetching) {
fetchNextPage();
}
});
if (node) observer.current.observe(node);
},
[fetchNextPage, hasNextPage, isFetching, isLoading]
);
我们将收到节点,一些div
要观察的元素。
首先,我验证状态是否为 Loading,如果是,我简单地不返回任何内容并退出该函数。
现在我验证我是否已经拥有 IntersectionObserver 的实例。如果已经有,我会断开连接,因为我不想创建观察者的多个实例。
现在如果我们没有。让我们将箭头函数的参数new IntersectionObserver()
传递给它。entries
现在我们将验证页面是否相交、是否有下一页并且未获取。
如果所有这些条件都得到验证,我将调用fetchNextPage()
该useInfiniteQuery
函数返回的值。
现在让我们传递观察引用node
。
就是这样!一个小怪物,不是吗?但如果我们冷静地阅读,就会发现事情并没有那么复杂。
const todos = useMemo(() => {
return data?.pages.reduce((acc, page) => {
return [...acc, ...page];
}, []);
}, [data]);
现在让我们验证并返回可能的状态并返回值:
if (isLoading) return <h1>Loading...</h1>;
if (error) return <h1>Error on fetch data...</h1>;
return (
<div>
{todos &&
todos.map((item) => (
<div key={item.id} ref={lastElementRef}>
<p>{item.title}</p>
</div>
))}
{isFetching && <div>Fetching more data...</div>}
</div>
在简历中我们将有这个组件:
src/Todos/index.tsx
import { useCallback, useMemo, useRef } from "react";
import { useInfiniteQuery } from "react-query";
const MAX_POST_PAGE = 10;
interface TodoType {
id: number;
title: string;
}
const fetchTodos = async ({ pageParam }: { pageParam: number }) => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos?_pages=${pageParam}&_limit=${MAX_POST_PAGE}`
);
const todos = (await response.json()) as TodoType[];
return todos;
};
export const Todo = () => {
const observer = useRef<IntersectionObserver>();
const { data, error, fetchNextPage, hasNextPage, isFetching, isLoading } =
useInfiniteQuery({
queryKey: ["todos"],
queryFn: ({ pageParam }) => fetchTodos({ pageParam }),
getNextPageParam: (lastPage, allPages) => {
return lastPage.length ? allPages.length + 1 : undefined;
},
});
const lastElementRef = useCallback(
(node: HTMLDivElement) => {
if (isLoading) return;
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasNextPage && !isFetching) {
fetchNextPage();
}
});
if (node) observer.current.observe(node);
},
[fetchNextPage, hasNextPage, isFetching, isLoading]
);
const todos = useMemo(() => {
return data?.pages.reduce((acc, page) => {
return [...acc, ...page];
}, []);
}, [data]);
if (isLoading) return <h1>Carregando mais dados...</h1>;
if (error) return <h1>Erro ao carregar os dados</h1>;
return (
<div>
{todos &&
todos.map((item) => (
<div key={item.id} ref={lastElementRef}>
<p>{item.title}</p>
</div>
))}
{isFetching && <div>Carregando mais dados...</div>}
</div>
);
};
现在,main.tsx
我将替换App.tsx
之前示例的内容来呈现我们的 Todo 组件:
src/main.tsx
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<Todo />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</React.StrictMode>
现在我们的无限滚动就做好了
我正在参与2024腾讯技术创作特训营第五期有奖征文,快来和我瓜分大奖!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。