探索 React 中的无限滚动技术

445 阅读3分钟

限滚动已成为 Web 开发中的一种流行技术,可在处理大量数据时提供无缝和动态的用户体验。它允许用户无休止地滚动浏览内容,而无需显式分页或加载新页面。在这篇博客中,我们将探讨如何在 React 应用程序中实现无限滚动,利用其虚拟化功能并优化性能。

在 React 中有 3 种方法可以实现无限滚动。

1.使用React库

要开始在您的 React 应用程序中实现无限滚动,我们需要安装一些依赖项。为此目的最常用的库是 react-infinite-scroll-component。你可以使用 npm 或 yarn 安装它,

npm install react-infinite-scroll-component axios
或者
yarn add react-infinite-scroll-component axios

之后我们需要导入组件,

import React, { useState, useEffect } from "react";
import InfiniteScroll from "react-infinite-scroll-component";

设置我们组件的初始状态。这包括项目列表、加载标志和存储下一页索引的变量。

import React, { useState, useEffect } from "react";
import InfiniteScroll from "react-infinite-scroll-component";
import axios from "axios";

const InfiniteScrollExample1 = () => {
  const [items, setItems] = useState([]);
  const [hasMore, setHasMore] = useState(true);
  const [index, setIndex] = useState(2);

  // Rest of the components

现在让我们看看如何从后端获取数据。这里我们使用 Axios 库和Platzi Fake Store来获取虚拟数据。

所以代码中有两部分。首先,使用useEffect挂钩,我们从 API 检索初始产品集,并items使用已解析的 API 响应数据更新状态变量。

第二部分,fetchMoreData函数被单独定义来处理当用户到达页面末尾或触发特定事件时获取更多数据。
当新数据返回时,它会将其添加到 items 变量中的现有产品中。它还会检查是否还有更多产品需要加载,如果有,它会设置一个名为 的变量hasMoretrue以便我们知道可以稍后加载更多产品。
并在函数结束时更新状态index

  useEffect(() => {
    axios
      .get("https://api.escuelajs.co/api/v1/products?offset=10&limit=12")
      .then((res) => setItems(res.data))
      .catch((err) => console.log(err));
  }, []);

  const fetchMoreData = () => {
    axios
      .get(`https://api.escuelajs.co/api/v1/products?offset=${index}0&limit=12`)
      .then((res) => {
        setItems((prevItems) => [...prevItems, ...res.data]);

        res.data.length > 0 ? setHasMore(true) : setHasMore(false);
      })
      .catch((err) => console.log(err));

    setIndex((prevIndex) => prevIndex + 1);
  };

然后将项目列表包装在 InfiniteScroll 组件中。通过传递必要的道具(如 dataLength、next、hasMore 和 loader)来配置组件

 return (
    <InfiniteScroll
      dataLength={items.length}
      next={fetchMoreData}
      hasMore={hasMore}
      loader={<Loader />}
    >
      <div className='container'>
        <div className='row'>
          {items &&
            items.map((item) => <ProductCard data={item} key={item.id} />)}
        </div>
      </div>
    </InfiniteScroll>
  );

这样一切就完成了:

import React, { useState, useEffect } from "react";
import InfiniteScroll from "react-infinite-scroll-component";
import axios from "axios";
import ProductCard from "./ProductCard";
import Loader from "./Loader";

const InfiniteScrollExample1 = () => {
  const [items, setItems] = useState([]);
  const [hasMore, setHasMore] = useState(true);
  const [index, setIndex] = useState(2);

  useEffect(() => {
    axios
      .get("https://api.escuelajs.co/api/v1/products?offset=10&limit=12")
      .then((res) => setItems(res.data))
      .catch((err) => console.log(err));
  }, []);

  const fetchMoreData = () => {
    axios
      .get(`https://api.escuelajs.co/api/v1/products?offset=${index}0&limit=12`)
      .then((res) => {
        setItems((prevItems) => [...prevItems, ...res.data]);

        res.data.length > 0 ? setHasMore(true) : setHasMore(false);
      })
      .catch((err) => console.log(err));

    setIndex((prevIndex) => prevIndex + 1);
  };

  return (
    <InfiniteScroll
      dataLength={items.length}
      next={fetchMoreData}
      hasMore={hasMore}
      loader={<Loader />}
    >
      <div className='container'>
        <div className='row'>
          {items &&
            items.map((item) => <ProductCard data={item} key={item.id} />)}
        </div>
      </div>
    </InfiniteScroll>
  );
};

export default InfiniteScrollExample1;

2. 构建自定义解决方案

如果您更喜欢自定义解决方案,则可以通过手动处理滚动事件来实现无限滚动。让我们看看代码

import React, { useState, useEffect, useCallback } from "react";
import axios from "axios";
import ProductCard from "./ProductCard";
import Loader from "./Loader";

const InfiniteScrollExample2 = () => {
  const [items, setItems] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [index, setIndex] = useState(2);

  // Rest of the component

我们可以使用 useCallback 挂钩定义 fetchData 函数来处理数据获取

  const fetchData = useCallback(async () => {
    if (isLoading) return;

    setIsLoading(true);

    axios
      .get(`https://api.escuelajs.co/api/v1/products?offset=${index}0&limit=12`)
      .then((res) => {
        setItems((prevItems) => [...prevItems, ...res.data]);
      })
      .catch((err) => console.log(err));
    setIndex((prevIndex) => prevIndex + 1);

    setIsLoading(false);
  }, [index, isLoading]);

我们可以使用 useEffect 钩子获取初始数据

useEffect(() => {
    const getData = async () => {
      setIsLoading(true);
      try {
        const response = await axios.get(
          "https://api.escuelajs.co/api/v1/products?offset=10&limit=12"
        );
        setItems(response.data);
      } catch (error) {
        console.log(error);
      }
      setIsLoading(false);
    };

    getData();
  }, []);

接下来,我们处理滚动事件,并在用户到达页面末尾时调用 fetchData 函数

  useEffect(() => {
    const handleScroll = () => {
      const { scrollTop, clientHeight, scrollHeight } =
        document.documentElement;
      if (scrollTop + clientHeight >= scrollHeight - 20) {
        fetchData();
      }
    };

    window.addEventListener("scroll", handleScroll);
    return () => {
      window.removeEventListener("scroll", handleScroll);
    };
  }, [fetchData]);

最后,渲染项目列表以及加载器组件

return (
    <div className='container'>
      <div className='row'>
        {items.map((item) => (
          <ProductCard data={item} key={item.id} />
        ))}
      </div>
      {isLoading && <Loader />}
    </div>
  );
};

这样一切就完成了:

import React, { useState, useEffect, useCallback } from "react";
import axios from "axios";
import ProductCard from "./ProductCard";
import Loader from "./Loader";

const InfiniteScrollExample2 = () => {
  const [items, setItems] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [index, setIndex] = useState(2);

  const fetchData = useCallback(async () => {
    if (isLoading) return;

    setIsLoading(true);

    axios
      .get(`https://api.escuelajs.co/api/v1/products?offset=${index}0&limit=12`)
      .then((res) => {
        setItems((prevItems) => [...prevItems, ...res.data]);
      })
      .catch((err) => console.log(err));
    setIndex((prevIndex) => prevIndex + 1);

    setIsLoading(false);
  }, [index, isLoading]);

  useEffect(() => {
    const getData = async () => {
      setIsLoading(true);
      try {
        const response = await axios.get(
          "https://api.escuelajs.co/api/v1/products?offset=10&limit=12"
        );
        setItems(response.data);
      } catch (error) {
        console.log(error);
      }
      setIsLoading(false);
    };

    getData();
  }, []);

  useEffect(() => {
    const handleScroll = () => {
      const { scrollTop, clientHeight, scrollHeight } =
        document.documentElement;
      if (scrollTop + clientHeight >= scrollHeight - 20) {
        fetchData();
      }
    };

    window.addEventListener("scroll", handleScroll);
    return () => {
      window.removeEventListener("scroll", handleScroll);
    };
  }, [fetchData]);

  return (
    <div className='container'>
      <div className='row'>
        {items.map((item) => (
          <ProductCard data={item} key={item.id} />
        ))}
      </div>
      {isLoading && <Loader />}
    </div>
  );
};

export default InfiniteScrollExample2;

3. 利用 Intersection Observer API

实现无限滚动的另一种方法是利用 Intersection Observer API。
Intersection Observer API 是一种现代开发技术,可以检测元素何时出现,从而触发内容加载以实现无限滚动。
Intersection Observer API 观察目标元素与祖先或视图元素相交的变化,使其非常适合实现无限滚动。
让我们看看如何实现

import React, { useState, useEffect, useRef, useCallback } from "react";
import axios from "axios";

const InfiniteScrollExample3 = () => {
  const [items, setItems] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [index, setIndex] = useState(2);
  const loaderRef = useRef(null);

   // Rest of the code

useEffect我们可以使用钩子获取初始数据

useEffect(() => {
    const getData = async () => {
      setIsLoading(true);
      try {
        const response = await axios.get(
          "https://api.escuelajs.co/api/v1/products?offset=10&limit=12"
        );
        setItems(response.data);
      } catch (error) {
        console.log(error);
      }
      setIsLoading(false);
    };

    getData();
  }, []);

函数fetchData是用钩子创建的一种特殊类型的函数useCallback。它会记住它的定义,并且只有在它的依赖项(在本例中index是 和isLoading)发生变化时才会发生变化。
其目的是在 API 被调用时处理来自 API 的额外数据检索。

const fetchData = useCallback(async () => {
  if (isLoading) return;

  setIsLoading(true);
  axios
    .get(`https://api.escuelajs.co/api/v1/products?offset=${index}0&limit=12`)
    .then((res) => {
      setItems((prevItems) => [...prevItems, ...res.data]);
    })
    .catch((err) => console.log(err));

  setIndex((prevIndex) => prevIndex + 1);

  setIsLoading(false);
}, [index, isLoading]);

useEffect钩子用于配置 intersection watcher,它监视加载元素在视口中的可见性。当显示正在加载的项目时,表明用户已向下滚动,fetchData调用该函数以获取其他数据。
cleanup 函数确保当组件不再使用时不会观察到 loader 项,以避免不必要的观察。

  useEffect(() => {
    const observer = new IntersectionObserver((entries) => {
      const target = entries[0];
      if (target.isIntersecting) {
        fetchData();
      }
    });

    if (loaderRef.current) {
      observer.observe(loaderRef.current);
    }

    return () => {
      if (loaderRef.current) {
        observer.unobserve(loaderRef.current);
      }
    };
  }, [fetchData]);

最后,渲染项目列表以及加载器组件

  return (
    <div className='container'>
      <div className='row'>
        {items.map((item, index) => (
          <ProductCard data={item} key={item.id} />
        ))}
      </div>
      <div ref={loaderRef}>{isLoading && <Loader />}</div>
    </div>
  );

这样一切就完成了:

import React, { useState, useEffect, useRef, useCallback } from "react";
import axios from "axios";
import ProductCard from "./ProductCard";
import Loader from "./Loader";

const InfiniteScrollExample3 = () => {
  const [items, setItems] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [index, setIndex] = useState(2);
  const loaderRef = useRef(null);

  const fetchData = useCallback(async () => {
    if (isLoading) return;

    setIsLoading(true);
    axios
      .get(`https://api.escuelajs.co/api/v1/products?offset=${index}0&limit=12`)
      .then((res) => {
        setItems((prevItems) => [...prevItems, ...res.data]);
      })
      .catch((err) => console.log(err));

    setIndex((prevIndex) => prevIndex + 1);

    setIsLoading(false);
  }, [index, isLoading]);

  useEffect(() => {
    const observer = new IntersectionObserver((entries) => {
      const target = entries[0];
      if (target.isIntersecting) {
        fetchData();
      }
    });

    if (loaderRef.current) {
      observer.observe(loaderRef.current);
    }

    return () => {
      if (loaderRef.current) {
        observer.unobserve(loaderRef.current);
      }
    };
  }, [fetchData]);

  useEffect(() => {
    const getData = async () => {
      setIsLoading(true);
      try {
        const response = await axios.get(
          "https://api.escuelajs.co/api/v1/products?offset=10&limit=12"
        );
        setItems(response.data);
      } catch (error) {
        console.log(error);
      }
      setIsLoading(false);
    };

    getData();
  }, []);

  return (
    <div className='container'>
      <div className='row'>
        {items.map((item, index) => (
          <ProductCard data={item} key={item.id} />
        ))}
      </div>
      <div ref={loaderRef}>{isLoading && <Loader />}</div>
    </div>
  );
};

export default InfiniteScrollExample3;