React自定义Hook调用fetchMore时状态不持久异常修复
问题说明
实现的分页请求自定义Hook存在两个已知异常:
- 调用
fetchMore时,Hook内部读取到的当前状态始终为空对象,无法获取上一页的next请求地址构造请求 - 调用
fetchMore后,组件拿到的data是pending状态的Promise,无法正常渲染列表
根因分析
- 过时闭包问题:
fetchMore通过useCallback包裹且依赖数组为空,函数在组件首次渲染时就被创建,闭包内捕获的data永远是初始状态的空对象,后续data更新不会触发fetchMore重新创建,因此函数内部直接读取外层data永远拿到初始值。 - 错误使用状态更新API:React的
useState更新函数不支持传入异步updater,当给setData传入async (prev) => {...}时,React不会等待该异步函数执行完成,会直接把async函数返回的Promise对象作为新状态存入state,最终导致state变成pending状态的Promise。
修复方案
- 移除
setData中的异步updater写法,将接口请求逻辑放到setData外部执行,请求完成拿到结果后再同步调用setData做状态合并。 - 使用
useRef存储最新的data状态,保证fetchMore执行时始终能拿到最新的状态值,不需要把data加入useCallback依赖,保证fetchMore引用稳定。 - 调整组件侧的
useCallback依赖,移除不必要的data依赖,避免回调函数不必要的重复创建。
修复后完整代码
自定义Hook(useFetch.js)
import { useEffect, useState, useCallback, useRef } from "react" const fetchPokemons = async (url) => { const res = await fetch(url ?? 'https://pokeapi.co/api/v2/pokemon') return await res.json() } export const useFetch = () => { const [data, setData] = useState({}) // 用ref始终保存最新的data值,解决闭包陈旧值问题 const dataRef = useRef(data) dataRef.current = data const setList = useCallback(async () => { const res = await fetchPokemons() setData(res) }, []) const fetchMore = useCallback(async () => { // 从ref中读取最新状态,不会被闭包捕获旧值 const currentData = dataRef.current console.log("🚀 ~ fetchMore ~ currentData", currentData) // 没有下一页地址直接返回,避免无效请求 if (!currentData.next) return // setData外部执行异步请求 const res = await fetchPokemons(currentData.next) // 请求完成后同步调用setData合并状态 setData((prev) => ({ ...prev, ...res, results: [...(prev.results || []), ...res.results] })) }, []) useEffect(() => { setList() return () => { setData({}) } }, [setList]) return [data, fetchMore] }
组件代码(App.jsx)
import { useCallback, useState } from 'react' import logo from './images/webpack.png' import { useFetch } from './hooks/useFetch' export const App = () => { const [count, setCount] = useState(0) const [data, fetchMore] = useFetch() const { results } = data console.log("🚀 ~ App ~ data", data) // 移除不必要的data依赖,fetchMore引用稳定时不需要重复创建回调 const incrementCount = useCallback(async () => { setCount((prev) => prev + 1) await fetchMore() }, [fetchMore]) return ( <div className='wrapper'> <header className='header'> <h1>React app! {count}</h1> <button className='increment' onClick={incrementCount}>+</button> </header> <section> <a href='https://webpack.js.org/'> <img className='logo' src={logo} alt="Webpack5" width={200} /> </a> {results?.map((r) => <p key={r.name}>{r.name}</p>)} </section> </div> ) }
补充说明
- 不要在
useState的updater函数中执行异步逻辑,updater应该是纯同步函数,接收前序状态、返回新状态即可;React在严格模式下可能多次执行updater,写异步逻辑会导致重复请求等问题。 - 如果
useCallback包裹的函数需要使用某个状态,但又不希望状态变化导致函数引用变更,用useRef存储最新状态是通用解决方案。
内容的提问来源于stack exchange,提问作者Diego
相关产品推荐
相关产品推荐

