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

React自定义Hook调用fetchMore时状态不持久异常修复

问题说明

实现的分页请求自定义Hook存在两个已知异常:

  • 调用fetchMore时,Hook内部读取到的当前状态始终为空对象,无法获取上一页的next请求地址构造请求
  • 调用fetchMore后,组件拿到的data是pending状态的Promise,无法正常渲染列表
根因分析
  1. 过时闭包问题:fetchMore通过useCallback包裹且依赖数组为空,函数在组件首次渲染时就被创建,闭包内捕获的data永远是初始状态的空对象,后续data更新不会触发fetchMore重新创建,因此函数内部直接读取外层data永远拿到初始值。
  2. 错误使用状态更新API:React的useState更新函数不支持传入异步updater,当给setData传入async (prev) => {...}时,React不会等待该异步函数执行完成,会直接把async函数返回的Promise对象作为新状态存入state,最终导致state变成pending状态的Promise。
修复方案
  1. 移除setData中的异步updater写法,将接口请求逻辑放到setData外部执行,请求完成拿到结果后再同步调用setData做状态合并。
  2. 使用useRef存储最新的data状态,保证fetchMore执行时始终能拿到最新的状态值,不需要把data加入useCallback依赖,保证fetchMore引用稳定。
  3. 调整组件侧的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 13:48:19