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

React Hook中setState更新异常:useWidgets内skip始终为0的问题

React自定义Hook中的闭包与状态访问问题

自定义Hook代码

const useWidgets = () => {
    const [skip, setSkip] = useState(0);
    const take = 20;

    async function getItems() {
        try {
            console.log('skip is always 0 here', skip);

            const rsp = await apiGetWidgets({ skip });
            setSkip((state) => state + take);
        } catch (err) {
            console.log(err);
        }
    }

    useEffect(() => {
        getItems();
    }, []);

    return {  getItems, skip };
};

消费组件代码

const Infinite: FC = () => {
    const { getItems, skip } = useWidgets();

    
    useEffect(()=> {
        setTimeout(getItems, 4000)
    }, [])
    
    console.log('skip is updated here to latest value', skip);

    return <div>
        {skip}
    </div>;
};

核心问题解析

  1. getItems里的skip始终为0,确实是闭包问题
    getItems在useWidgets第一次执行时被创建,它捕获了当时的skip初始值0。你在Infinite组件的useEffect里只在初始渲染时调用了setTimeout,传入的是第一次生成的getItems——这个函数的闭包环境固定在初始状态,后续不管skip怎么更新,它访问到的永远是0。

  2. useWidgets每次被调用,skip不会重新初始化
    React会在组件多次渲染之间保留Hook的状态,所以每次Infinite重新渲染时,useWidgets里的useState(0)不会把skip重置为0,而是返回上一次更新后的状态。这也是Infinite能拿到最新skip的原因:每次渲染时,useWidgets都会返回当前最新的skip,以及当前渲染周期生成的新getItems函数,但你调用的始终是第一次的版本。

  3. 消费组件能拿到最新skip,Hook内的getItems不行的原因

  • Infinite每次重新渲染,都会从useWidgets获取React维护的最新skip状态,所以能打印出最新值。
  • 你调用的getItems是初始渲染时的版本,它的闭包只绑定了当时的skip=0;后续渲染生成的新getItems虽然能访问最新skip,但你并没有调用这些新函数。

修复方案

方案一:用useCallback包裹getItems,更新依赖

让getItems在skip变化时重新生成,确保每次调用的都是能访问最新skip的版本:

const useWidgets = () => {
    const [skip, setSkip] = useState(0);
    const take = 20;

    const getItems = useCallback(async () => {
        try {
            console.log('skip now is', skip);
            const rsp = await apiGetWidgets({ skip });
            setSkip((state) => state + take);
        } catch (err) {
            console.log(err);
        }
    }, [skip]); // 依赖skip,变化时重新生成函数

    useEffect(() => {
        getItems();
    }, [getItems]); // 依赖更新后的getItems

    return { getItems, skip };
};

同时更新Infinite组件的useEffect依赖,确保setTimeout使用最新的getItems:

const Infinite: FC = () => {
    const { getItems, skip } = useWidgets();

    useEffect(()=> {
        const timer = setTimeout(getItems, 4000)
        return () => clearTimeout(timer); // 清理定时器避免内存泄漏
    }, [getItems])
    
    console.log('skip is updated here to latest value', skip);

    return <div>
        {skip}
    </div>;
};

方案二:用useRef保存最新的skip值

如果不想频繁重新生成getItems,可以用useRef跟踪skip的最新值,确保任何时候调用都能拿到最新状态:

const useWidgets = () => {
    const [skip, setSkip] = useState(0);
    const skipRef = useRef(skip);
    const take = 20;

    // 每次skip更新时同步到ref
    useEffect(() => {
        skipRef.current = skip;
    }, [skip]);

    async function getItems() {
        try {
            console.log('skip now is', skipRef.current);
            const rsp = await apiGetWidgets({ skip: skipRef.current });
            setSkip((state) => state + take);
        } catch (err) {
            console.log(err);
        }
    }

    useEffect(() => {
        getItems();
    }, []);

    return { getItems, skip };
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 16:26:08