You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在嵌套分页数据请求中正确使用React的useEffect Hook

优化React中基于分页API的书籍数据获取逻辑

问题场景

  • 通过自定义Hook获取带分页逻辑的作者列表,每条作者数据结构:{ id: integer, name: string, books: Book[]},其中Book仅包含id字段
  • 需要根据书籍ID请求完整书籍数据,返回结构:{id: integer, author_id: integer, name: string}
  • API分页限制为12条,必须自动请求所有页面以补全数据

现有代码

const customHook = () => { /* returns books for us */ }

const BooksByAuthorPage = () => {
  const { authors } = customHook();
  const [booksByAuthor, setBooksByAuthor] = useState({});

  useEffect(() => {  
    const fetchPage = async (page, bookIds) => {
      const bookUrl = `/api/books?ids=${bookIds}&page=${page}`;
      const response = await fetch(bookUrl);
    
      // The first value is the list of books for that page
      // The second value indicates if there is a next page to fetch from
      return [response.body.data, response.body.links.next];
    }

    const fetchAllPages = async (bookIds) => {
      let page = 1;
      let next = true;
      const booksByAuthorId = {};

      while (next) {
        let [books, next] = fetchPage(page, bookIds)
        next = next;
        books.forEach(book => {
          if (booksByAuthorId[book.author_id]) {
            booksByAuthorId[book.author_id].push(book);
          } else {
            booksByAuthorId[book.author_id] = [book];
          }
        });

        setBooksByAuthor((prevBooks) => {
          const newBooksByAuthor = { ...prevBooks };

          Object.keys(booksByAuthorId).forEach((authorId) => {
            const newBooks = booksByAuthorId[authorId];
            // If the author id is already added as a key
            if (newBooksByAuthor[authorId]) {
              // If the book does not yet exist on the author then add it
              newBooks.forEach((newBook) => {
                if (
                  !newBooksByAuthor[authorId].find(
                    (existingBook) => existingBook.id === newBook.id
                  )
                ) {
                  newBooksByAuthor[authorId].push(newBook);
                }
              });
            // Otherwise add the new key with the list
            } else {
              newBooksByAuthor[authorId] = [newBook];
            }
          });

          return {
            ...newBooksByAuthor,
          };
        });
      }
      
      // trigger the next paginated request if needed
      if (next) { 
        page += 1 
      }
    }

    const bookIds = authors.flatMap(a => a.books.map(b => b.id));
    fetchAllPages(bookIds);
  }, [authors]);
};

现有代码核心问题

  • 每分页一次就更新状态,需要维护临时变量booksByAuthorId并与现有状态比对去重,逻辑冗余且容易出错
  • 使用可变的page、next变量,不符合函数式编程风格,循环逻辑存在死循环风险
  • 未处理请求错误,也没有中断旧请求的机制,当authors变化时旧请求可能覆盖新状态
  • 存在语法bug:状态更新的else分支中newBook未定义,应为newBooks

优化方案

1. 先拉取全量数据再一次性更新状态

取消每页更新状态的逻辑,等所有分页数据拉取完成后,统一处理分组和去重,再更新状态,减少状态更新次数,简化逻辑:

const BooksByAuthorPage = () => {
  const { authors } = customHook();
  const [booksByAuthor, setBooksByAuthor] = useState({});
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    const signal = controller.signal;

    const fetchAllBooks = async () => {
      setIsLoading(true);
      setError(null);
      const bookIds = authors.flatMap(a => a.books.map(b => b.id));
      if (bookIds.length === 0) {
        setBooksByAuthor({});
        setIsLoading(false);
        return;
      }

      // 递归拉取所有分页数据
      const fetchPage = async (page = 1, accumulated = []) => {
        try {
          const res = await fetch(`/api/books?ids=${bookIds}&page=${page}`, { signal });
          if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
          const { data: books, links } = await res.json();
          
          const allBooks = [...accumulated, ...books];
          // 存在下一页则继续递归
          if (links.next) {
            return fetchPage(page + 1, allBooks);
          }
          return allBooks;
        } catch (err) {
          if (err.name !== 'AbortError') throw err;
        }
      };

      try {
        const allBooks = await fetchPage();
        // 按author_id分组并去重
        const grouped = allBooks.reduce((acc, book) => {
          const authorKey = book.author_id.toString();
          if (!acc[authorKey]) acc[authorKey] = [];
          // 避免重复添加同一本书
          if (!acc[authorKey].some(b => b.id === book.id)) {
            acc[authorKey].push(book);
          }
          return acc;
        }, {});
        setBooksByAuthor(grouped);
      } catch (err) {
        setError(err.message);
      } finally {
        setIsLoading(false);
      }
    };

    fetchAllBooks();
    // 组件卸载或authors变化时中断请求
    return () => controller.abort();
  }, [authors]);

  // 渲染逻辑...
};

2. 遵循函数式编程风格

用递归替代while循环,避免使用可变变量;所有状态更新通过setState完成,不依赖外部可变状态。

3. 增强健壮性

  • 添加加载状态isLoading和错误状态error,提升用户体验
  • 使用AbortController中断旧请求,避免状态覆盖或内存泄漏
  • 处理HTTP错误和请求中断错误
  • 空数组判断,避免无意义的请求

关于Hook使用的理解

你并没有错误使用useEffect——监听authors变化触发数据请求是完全合理的副作用场景。问题出在数据处理和状态更新的时机上:没必要每一页都更新状态,应该等数据完整后一次性处理,同时要处理请求中断的边界情况。

API层面的优化建议

如果可以调整API,建议:

  • 提供批量获取接口,支持一次传入所有书籍ID,内部处理分页并返回全量数据,减少前端请求次数
  • 支持作者列表接口关联返回书籍数据(比如通过?include=books参数),避免额外请求
  • 分页响应中明确返回has_next布尔值,替代依赖links.next的存在性判断,逻辑更清晰

内容的提问来源于stack exchange,提问作者Jared

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 10:34:59