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>; };
核心问题解析
getItems里的skip始终为0,确实是闭包问题
getItems在useWidgets第一次执行时被创建,它捕获了当时的skip初始值0。你在Infinite组件的useEffect里只在初始渲染时调用了setTimeout,传入的是第一次生成的getItems——这个函数的闭包环境固定在初始状态,后续不管skip怎么更新,它访问到的永远是0。useWidgets每次被调用,skip不会重新初始化
React会在组件多次渲染之间保留Hook的状态,所以每次Infinite重新渲染时,useWidgets里的useState(0)不会把skip重置为0,而是返回上一次更新后的状态。这也是Infinite能拿到最新skip的原因:每次渲染时,useWidgets都会返回当前最新的skip,以及当前渲染周期生成的新getItems函数,但你调用的始终是第一次的版本。消费组件能拿到最新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
相关产品推荐
相关产品推荐

