使用交叉观察器在 React 中实现无限滚动

2025-05-24

使用交叉观察器在 React 中实现无限滚动

大家好,

几天前,我偶然发现了 React 中无限滚动的用例。为此,我使用了 Intersection Observer,并找到了在无限滚动中实现它的不同方法。

在深入探讨之前,我们先来更好地理解一下问题描述。假设有一个 API,它提供用户列表及其一些基本信息。我们的任务是在卡片中显示所有用户的列表。很简单吧?

现在,假设有成千上万的用户,并且我们使用的 API 是分页的。在这种情况下,有两种方法可以使用我们的分页 API:

  1. 使用下一个/上一个按钮浏览不同的页面
  2. 使用无限滚动

正如文章标题所说,我们将采用第二种方法。😅
现在,让我们看看如何做?

  1. 我们将调用我们的 API 来获取前 25 个结果。
  2. 一旦用户滚动列表并到达最后一个元素,我们将进行另一次 API 调用并在视图中提取下一组用户。

这样,即使用户继续滚动,他们也始终会看到用户列表,直到到达最后。

在进入实现部分之前,让我先简单介绍一下 Intersection Observer

什么是交叉口观察器?

交叉观察器是一种浏览器 API,它提供了一种异步观察或检测两个元素相互之间可见性的方法。

根据 MDN,此 API 主要用于执行与可见性相关的任务,包括图像的延迟加载和实现“无限滚动”网站,其中滚动时会加载和呈现越来越多的内容。

您可以在此处查看交叉口观察器的详细信息

实现无限滚动

对于无限滚动,我们将使用开源RandomUserAPI

为了进行基本的项目设置,我使用create-react-app创建了一个简单的 React 项目,并添加了Tailwind CSS。此外,为了调用 API,我还在同一个项目中添加了axios 。

我将实施过程分为以下两个步骤 -

1.调用API,存储并展示数据。

完成基本设置后,让我们看一下代码的第一个版本,其中我们调用用户 API 来获取用户列表。



// app.js
import axios from 'axios';
import { useEffect, useState } from 'react';

const TOTAL_PAGES = 3;

const App = () => {
    const [loading, setLoading] = useState(true);
    const [allUsers, setAllUsers] = useState([]);
    const [pageNum, setPageNum] = useState(1);

    const callUser = async () => {
        setLoading(true);
        let response = await axios.get(
            `https://randomuser.me/api/?page=${pageNum}&results=25&seed=abc`
        );
        setAllUsers(response.data.results);
        setLoading(false);
    };

    useEffect(() => {
        if (pageNum <= TOTAL_PAGES) {
            callUser();
        }
    }, [pageNum]);

    const UserCard = ({ data }) => {
        return (
            <div className='p-4 border border-gray-500 rounded bg-white flex items-center'>
                <div>
                    <img
                        src={data.picture.medium}
                        className='w-16 h-16 rounded-full border-2 border-green-600'
                        alt='user'
                    />
                </div>

                <div className='ml-3'>
                    <p className='text-base font-bold'>
                        {data.name.first} {data.name.last}
                    </p>
                    <p className='text-sm text-gray-800'>
                        {data.location.city}, {data.location.country}
                    </p>
                    <p className='text-sm text-gray-500 break-all'>
                        {data.email}
                    </p>
                </div>
            </div>
        );
    };

    return (
        <div className='mx-44 bg-gray-100 p-6'>
            <h1 className='text-3xl text-center mt-4 mb-10'>All users</h1>

            <div className='grid grid-cols-3 gap-4'>
                {allUsers.length > 0 &&
                    allUsers.map((user, i) => {
                        return (
                            <div key={`${user.name.first}-${i}`}>
                                <UserCard data={user} />
                            </div>
                        );
                    })}
            </div>
            {loading && <p className='text-center'>loading...</p>}
        </div>
    );
};

export default App;



Enter fullscreen mode Exit fullscreen mode

我们的页面看起来是这样的👇
我们的页面看起来是这样的

代码非常简单。在callUser函数中,我们调用 API 并将结果存储在状态中。下面,我们使用卡片组件allUsers显示数组中的每个用户allUsersUserCard

您将看到组件顶部定义了一个const 变量TOTAL_PAGES,用于限制我们在整个应用程序中需要遍历的页面总数。在实际应用中,不需要这样做,因为 API 会提供可用页面总数的详细信息。

另外,你可能已经注意到,我们定义了一个状态来存储页码,但到目前为止,还没有正确使用它。这是因为我们想从交叉点观察器中更改这个页码。

2. 添加交叉口观察器并增加页码

要实现无限滚动,我们需要在列表的最后一个元素对用户可见时增加页码计数。这将由交叉观察器完成。

我们的交叉观察者将观察最后一个元素是否可见,如果可见,我们将页码增加 1。由于我们的 useEffect 将在页码发生变化时运行,因此 API 将被调用,因此我们将获得更多用户列表。

理解了这个逻辑之后,我们来看看工作代码——



// App.js

const App = () => {
    const [loading, setLoading] = useState(true);
    const [allUsers, setAllUsers] = useState([]);
    const [pageNum, setPageNum] = useState(1);
    const [lastElement, setLastElement] = useState(null);

    const observer = useRef(
        new IntersectionObserver(
            (entries) => {
                const first = entries[0];
                if (first.isIntersecting) {
                    setPageNum((no) => no + 1);
                }
            })
    );

    const callUser = async () => {
        setLoading(true);
        let response = await axios.get(
            `https://randomuser.me/api/?page=${pageNum}&results=25&seed=abc`
        );
        let all = new Set([...allUsers, ...response.data.results]);
        setAllUsers([...all]);
        setLoading(false);
    };

    useEffect(() => {
        if (pageNum <= TOTAL_PAGES) {
            callUser();
        }
    }, [pageNum]);

    useEffect(() => {
        const currentElement = lastElement;
        const currentObserver = observer.current;

        if (currentElement) {
            currentObserver.observe(currentElement);
        }

        return () => {
            if (currentElement) {
                currentObserver.unobserve(currentElement);
            }
        };
    }, [lastElement]);

    const UserCard = ({ data }) => {
        return (
            <div className='p-4 border border-gray-500 rounded bg-white flex items-center'>
                <div>
                    <img
                        src={data.picture.medium}
                        className='w-16 h-16 rounded-full border-2 border-green-600'
                        alt='user'
                    />
                </div>

                <div className='ml-3'>
                    <p className='text-base font-bold'>
                        {data.name.first} {data.name.last}
                    </p>
                    <p className='text-sm text-gray-800'>
                        {data.location.city}, {data.location.country}
                    </p>
                    <p className='text-sm text-gray-500 break-all'>
                        {data.email}
                    </p>
                </div>
            </div>
        );
    };

    return (
        <div className='mx-44 bg-gray-100 p-6'>
            <h1 className='text-3xl text-center mt-4 mb-10'>All users</h1>

            <div className='grid grid-cols-3 gap-4'>
                {allUsers.length > 0 &&
                    allUsers.map((user, i) => {
                        return i === allUsers.length - 1 &&
                            !loading &&
                            pageNum <= TOTAL_PAGES ? (
                            <div
                                key={`${user.name.first}-${i}`}
                                ref={setLastElement}
                            >
                                <UserCard data={user} />
                            </div>
                        ) : (
                            <UserCard
                                data={user}
                                key={`${user.name.first}-${i}`}
                            />
                        );
                    })}
            </div>
            {loading && <p className='text-center'>loading...</p>}

            {pageNum - 1 === TOTAL_PAGES && (
                <p className='text-center my-10'></p>
            )}
        </div>
    );
};


Enter fullscreen mode Exit fullscreen mode

让我们深入了解代码。

我们定义了相交观察器并将其存储在 const 中observer。相交观察器有一个回调函数,该函数接受所有相交对象的数组。但由于我们只传递最后一个元素,因此我们始终会检查该数组的第 0 个元素。如果该元素相交且可见,我们将增加页码。

我们添加了一个状态lastElement并将其初始化为null。在页面内部,我们将把数组的最后一个元素传递给此状态。

因此,当状态值lastElement发生变化时,会调用另一个 useEffect(使用lastElement依赖项数组)。在这个 useEffect 中,如果我们获取到 lastElement 的值,我们会将该元素传递给交集观察器进行观察。观察器会检查该元素的交集,并在发生这种情况时增加页面计数。

随着页码的变化,API 将被调用并获取更多用户。请注意我们所做的细微修改,以便将这些新用户添加到现有状态中并避免重复。

该应用程序将毫不费力地运行,您现在可以看到无限滚动的实际效果!🥁

就这样吧!如果你想查看完整的代码,可以访问我的Github 仓库

非常感谢你阅读这篇文章。欢迎留言告诉我你的想法。如果你喜欢我的文章,也可以在Twitter上联系我,或者请我喝杯咖啡。

*快乐编码并继续学习 🙌 *

文章来源:https://dev.to/hey_yogini/infinite-scrolling-in-react-with-intersection-observer-22fh
PREV
useAxios:使用 axios 调用 API 的简单自定义钩子
NEXT
以更干净的方式有条件地渲染 React 组件