如何在嵌套分页数据请求中正确使用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
相关产品推荐
相关产品推荐

