#2 React 查询:无限滚动

2025-06-09

#2 React 查询:无限滚动

大家好!你们好吗?还好吗?最近怎么样?

正如我在第一篇关于 React Query 的文章中所承诺的那样,我讨论了这个状态管理工具,它提供了许多功能,如分页、缓存、自动重新取回、预取等等。

在本文中,我将讨论 React 查询的惊人功能——无限滚动。

介绍

你可能已经在所有社交媒体上见过这个功能了,比如 X(不过我更喜欢 Twitter),或者 Facebook、LinkedIn……这些社交媒体没有分页功能,但可以无限滚动,数据会自动生成。你无需使用按钮(下一页或上一页)即可使用分页功能。

但下面的无限滚动是一种分页。

因此,考虑到这一点,让我们看看代码!!!

亲自动手

我将使用上一篇文章中创建的同一个项目。因此,我不会演示如何安装或配置 React 查询。如果您不了解,建议您阅读第一篇文章,其中我讲解并演示了如何安装、设置等。

您可以在这里找到:https://dev.to/kevin-uehara/1-react-query-introducing-pagination-caching-and-pre-fetching-21p8

考虑到这一点,我将预先假设您已经配置了项目或者您了解反应查询的基础知识。

我将使用相同的 Fake API 来提供数据。JSON 占位符,但这次我将使用Todo端点。

例如访问:https://jsonplaceholder.typicode.com/todos? _pages=0&_limit=10

JSON 占位符页面

因此,在之前的同一个项目中,正如我所说,让我们为我们的组件 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;
}


Enter fullscreen mode Exit fullscreen mode

因此我们的 Todo 类型限制为 10,它将仅使用 id 和 title。

现在让我们创建函数来获取数据:



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;
};


Enter fullscreen mode Exit fullscreen mode

注意,该函数将接收代表页码的 pageParam。我将以对象的形式接收并使用 destruct。

到目前为止我们的组件看起来是这样的:



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 <></>;


Enter fullscreen mode Exit fullscreen mode

现在让我们创建 Todo 组件的内容。

让我们创建一个观察者引用,使用useRef钩子并将 IntersectionObserver 类型作为泛型传递,如下所示:



const observer = useRef<IntersectionObserver>();


Enter fullscreen mode Exit fullscreen mode

Observerdesing pattern,如定义:



Observer is a software design pattern that defines a one-to-many dependency between objects so that when an object changes state, all of its dependents are notified and updated automatically.


Enter fullscreen mode Exit fullscreen mode

观察者,顾名思义,它会观察某个对象的状态。如果依赖项发生更新,正在监听(观察)它的对象就会收到通知。

但你可能会想🤔我为什么要解释所有这些概念。好吧,我们需要使用观察者来判断用户是否在页面的末尾,以便通过下一页参数获取新数据。

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

令人震惊的 Gif

让我们使用 React 查询的钩子useInfiniteQuery。它非常类似于useQuery



const { data, error, fetchNextPage, hasNextPage, isFetching, isLoading } =
    useInfiniteQuery({
      queryKey: ["todos"],
      queryFn: ({ pageParam }) => fetchTodos({ pageParam }),
      getNextPageParam: (lastPage, allPages) => {
        return lastPage.length ? allPages.length + 1 : undefined;
      },
    });


Enter fullscreen mode Exit fullscreen mode

我们将析构并获取数据、错误消息、fetchNextpage 函数、hasNextPage 属性、isFectching 和 isLoading 状态。

我们将传递键“todos” queryKey、函数 fetchTodosqueryFn并创建一个函数getNextPageParam来获取下一页,如果有数据则递增并验证。

现在让我们创建一个函数来观察用户是否到达页面末尾。



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]
  );


Enter fullscreen mode Exit fullscreen mode

如果你现在还不明白这个功能,别担心。请冷静地阅读。

我们将接收节点和一些div要观察的元素。

首先,我验证状态是否为“正在加载”,如果是,我简单地返回任何内容并退出函数。

现在我验证是否已经有了 IntersectionObserver 的实例。如果已经有了,我就断开连接,因为我不想创建多个观察者实例。

现在,如果我们没有。让我们通过new IntersectionObserver()传递entries箭头函数的参数来实例化它。现在我们将验证页面是否相交、是否有下一页以及是否正在获取。
如果所有这些条件都通过了验证,我将调用函数fetchNextPage()返回的结果useInfiniteQuery

现在让我们传递观察引用node

就是这样!它不是个小怪兽吗?不过,如果我们冷静地读下去,就会发现其实也没那么复杂。

海绵宝宝累了 gif

现在我将使用 reduce 来格式化我们的数据以简化我们的数据:



const todos = useMemo(() => {
    return data?.pages.reduce((acc, page) => {
      return [...acc, ...page];
    }, []);
  }, [data]);


Enter fullscreen mode Exit fullscreen mode

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



 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>


Enter fullscreen mode Exit fullscreen mode

在简历中我们将有这个部分:

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>
  );
};


Enter fullscreen mode Exit fullscreen mode

现在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>


Enter fullscreen mode Exit fullscreen mode

现在将得到结果:

无限滚动示例(包含 Todos)

现在我们有了无限卷轴!!! 多么神奇啊,不是吗?

好了,就这样吧!!!
希望你们喜欢这篇关于这个神奇工具 React Query 的第二篇文章。

祝你一切安好。
非常感谢你读到这里。

狗微笑gif

联系方式:
Youtube:https://www.youtube.com/@ueharakevin/
Linkedin:https://www.linkedin.com/in/kevin-uehara/
Instagram:https://www.instagram.com/uehara_kevin/
Twitter:https: //twitter.com/ueharaDev
Github:https: //github.com/kevinuehara

鏂囩珷鏉ユ簮锛�https://dev.to/kevin-uehara/2-react-query-infinite-scroll-1mg8
PREV
为什么与程序员的对话很困难
NEXT
通过改善习惯成为更好的开发人员——《原子习惯》的 7 个关键要点