如何使用 React Hooks 优雅的批量获取分页数据:`useFetchAll`

322 阅读4分钟

背景

在项目开发过程中,我们经常需要从服务器获取分页数据。有时候我们会遇到一些奇葩的需求,从服务器一次性获取某个查询条件下的所有数据,这时候如果数据量较大,一次性获取所有数据可能会导致性能问题。那么这时候我们就要考虑通过分页分批次获取数据,然后讲所有分页数据合并为最终想要的总数据,这时候我们就不得不要考虑通过控制并发请求的数量,以提高性能和用户体验。

目标

实现一个自定义 Hook useFetchAll,用于批量获取分页数据,并控制并发请求的数量。这个 Hook 应该具备以下功能:

  1. 获取所有分页数据:根据给定的 URL 和每页请求的数据量,获取所有分页数据。
  2. 控制并发请求数量:通过并发池限制同时进行的请求数量,避免一次性发出过多请求导致服务器压力过大或浏览器性能问题。
  3. 错误处理:在请求过程中处理可能出现的错误,并在组件卸载时取消未完成的请求。

代码实现

自定义 Hook:useFetchAll

import { useState, useEffect, useRef } from 'react';
import axios from 'axios';

const useFetchAll = (url, pageSize, concurrencyLimit) => {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const abortController = useRef(null);

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      setError(null);
      abortController.current = new AbortController();

      try {
        // 获取第一页数据,来确定总数
        const firstPageResponse = await axios.get(url, {
          params: { page: 1, pageSize },
          signal: abortController.current.signal,
        });
        const total = firstPageResponse.data.total;
        const totalPages = Math.ceil(total / pageSize);

        // 创建一个数组,包含所有页码
        const pages = Array.from({ length: totalPages }, (_, i) => i + 1);

        // 并发请求函数
        const fetchPage = async (page) => {
          try {
            const response = await axios.get(url, {
              params: { page, pageSize },
              signal: abortController.current.signal,
            });
            return { page, items: response.data.items };
          } catch (err) {
            if (axios.isCancel(err)) {
              console.log(`Request for page ${page} was cancelled`);
            } else {
              throw err;
            }
          }
        };

        // 控制并发请求
        const fetchAllPages = async () => {
          const results = [];
          const pool = new Set();

          for (const page of pages) {
            const promise = fetchPage(page).then((result) => {
              if (result) {
                results.push(result);
              }
              pool.delete(promise);
            });

            pool.add(promise);

            if (pool.size >= concurrencyLimit) {
              await Promise.race(pool);
            }
          }

          await Promise.all(pool);
          return results;
        };

        const allData = await fetchAllPages();
        // 按页码排序并合并数据
        const sortedData = allData
          .sort((a, b) => a.page - b.page)
          .flatMap(result => result.items);
        setData(sortedData);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    };

    fetchData();

    return () => {
      if (abortController.current) {
        abortController.current.abort();
      }
    };
  }, [url, pageSize, concurrencyLimit]);

  return { data, loading, error };
};

export default useFetchAll;

这段代码在干嘛?

  1. 初始化状态: 我们用 useState 创建了 dataloadingerror 三个状态变量,分别用于存储数据、加载状态和错误信息。

  2. 取消请求: 我们用 useRef 创建了一个 abortController,在组件卸载时取消未完成的请求,以避免内存泄漏。

  3. 获取第一页数据: 我们用 axios 请求第一页的数据,来确定总数。这样我们就知道总共有多少页需要请求。

  4. 创建页码数组: 根据总数和每页请求数,我们创建一个包含所有页码的数组。

  5. 并发请求: 我们定义了一个 fetchPage 函数,用于请求每一页的数据,并返回页码和数据。然后用一个并发池来控制同时进行的请求数量。

  6. 合并数据: 我们将所有页的数据按页码排序并合并成一个完整的数据集,并更新 data 状态。

  7. 错误处理: 在请求过程中,如果发生错误,我们会捕获并设置 error 状态。

  8. 取消请求: 在组件卸载时取消未完成的请求,以避免内存泄漏。

使用示例

下面是一个使用 useFetchAll Hook 的示例组件:

import React from 'react';
import useFetchAll from './useFetchAll';

const ExampleComponent = () => {
  const url = 'https://api.example.com/data';
  const pageSize = 10;
  const concurrencyLimit = 3;

  const { data, loading, error } = useFetchAll(url, pageSize, concurrencyLimit);

  if (loading) return <p>加载中...</p>;
  if (error) return <p>出错了: {error.message}</p>;

  return (
    <div>
      <h1>数据列表</h1>
      <ul>
        {data.map((item, index) => (
          <li key={index}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
};

export default ExampleComponent;

解释

  1. 定义 URL 和参数: 我们定义了数据请求的 URL、每页请求的数据量 pageSize 和并发请求的限制 concurrencyLimit

  2. 调用 useFetchAll: 我们调用 useFetchAll Hook,并解构出 dataloadingerror 三个状态。

  3. 处理加载和错误状态: 如果数据正在加载,显示加载提示;如果发生错误,显示错误信息。

  4. 渲染数据: 如果数据加载完成且没有错误,渲染数据列表。

最后

通过这个自定义 Hook useFetchAll,我们可以方便地批量获取分页数据,并控制并发请求的数量。这个 Hook 具有以下优点:

  1. 简化代码: 将复杂的分页请求逻辑封装在 Hook 中,使组件代码更加简洁。

  2. 提高性能: 通过控制并发请求数量,避免一次性发出过多请求导致的性能问题。

  3. 易于扩展: 可以根据需要对 Hook 进行扩展,例如添加更多的请求参数或处理不同的错误情况。

希望这个笔记对你有所帮助!Happy coding! 🎉