前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >React 查询:无限滚动

React 查询:无限滚动

原创
作者头像
泽霖
发布2024-01-30 23:53:11
1060
发布2024-01-30 23:53:11

在这篇文章中我们将谈谈 React Query 这个状态管理工具提供的一个令人惊叹的功能,即无限滚动(Infinite Scroll)。

介绍

可能你已经在每个社交媒体平台上看到了这个功能,比如 Twitter、Facebook、LinkedIn 等。在这些平台上,我们不再使用传统的分页,而是通过无限滚动来加载数据。没有上一页或下一页的按钮,数据会根据需要自动生成。但在底层,无限滚动仍然是分页的一种形式。

下面让我们看看代码吧!

上手

JSON Placeholder 页面##

首先,在我们的项目中创建 Todos 组件的文件夹:src/Todo/index.tsx

在其他情况下,我可能会创建一个 types.ts 文件来存放我们的类型,但这次我们只会在这个文件中使用。因此,我将在我们的组件中创建类型。此外,我将添加 MAX_POST_PAGE 常量。

代码语言:tsx
复制
// src/Todo/index.tsx
const MAX_POST_PAGE = 10;

interface TodoType {
  id: number;
  title: string;
}

因此,我们将有一个限制为 10 的 MAX_POST_PAGE 和我们的 Todo 类型,该类型只使用 id 和 title。

接下来让我们创建用于获取数据的函数:

代码语言:tsx
复制
// 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,表示页码。我会将其作为对象接收并使用解构。

代码语言:tsx
复制
// 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 类型作为泛型传递,如下所示:

代码语言:tsx
复制
// src/Todo/index.tsx
const observer = useRef<IntersectionObserver>();

观察者是一种设计模式,定义了对象之间的一对多依赖关系,以便当对象更改状态时,所有依赖项都会被通知并自动更新。观察者,顾名思义,将观察某个对象的状态。如果依赖项更新,正在监听(观察)的对象将被通知。

但你可能会想 🤔 为什么我要解释所有这些概念,我们将需要使用观察者来查看用户是否在页面底部,以便传递下一个页面参数时获取新数据。

所以,是的!正如我之前所说,无限滚动是一种不同类型的分页 🤯

让我们使用 React Query 的 useInfiniteQuery 钩子。它与 useQuery 非常相似:

代码语言:tsx
复制
// 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 属性、isFetchingisLoading 状态。

我们将在 queryKey 中传递键值 'todos',在 queryFn 中传递 fetchTodos 函数,并在 getNextPageParam 中创建一个函数来获取下一页,增加并验证我们是否有数据。

现在让我们创建一个函数来观察用户是否到达页面底部:

代码语言:tsx
复制
// 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

就是这样!一个小怪物,不是吗?但如果我们冷静地阅读,就会发现事情并没有那么复杂。

代码语言:javascript
复制
const todos = useMemo(() => {
    return data?.pages.reduce((acc, page) => {
      return [...acc, ...page];
    }, []);
  }, [data]);

现在让我们验证并返回可能的状态并返回值:

代码语言:javascript
复制
 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

代码语言:javascript
复制
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

代码语言:javascript
复制
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 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 介绍
  • 上手
  • JSON Placeholder 页面##
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档