React函数组件中如何获取更新后的state值?
React函数组件中无法获取更新后state值的问题
问题背景
在React函数组件中遇到无法获取更新后state值的问题,以下是相关代码示例及场景说明:
第一版代码
const TestApp = () => { const [speed, setSpeed] = useState(1); const incrementSpeed = () => { console.log(speed, `speed`); // 始终输出1,如何获取新的state? setSpeed(speed + 1); window.requestAnimationFrame(incrementSpeed); }; useEffect(() => { window.addEventListener('keydown', (e) => { if (e.key === 'z') { incrementSpeed(); } }); }, []); return <>></>; }; export default TestApp;
尝试现有方案后未解决问题,因此更新代码至第二版:
第二版代码
const TestApp = () => { const [speed, setSpeed] = useState(1); // 其他函数更新state后,该函数无法获取新state const incrementSpeed = () => { console.log(speed, `speed`); // 始终输出1,如何获取新的state? window.requestAnimationFrame(incrementSpeed); }; useEffect(() => { const handle = (e) => { if (e.key === 'o') { setSpeed((speed) => speed + 1); } else if (e.key === 'p') { incrementSpeed(); } }; window.addEventListener('keydown', handle); return () => window.removeEventListener('keydown', handle); }, [speed]); return <>></>; }; export default TestApp;
存在两个操作:
- 按下'o'键将speed加1
- 按下'p'键调用
incrementSpeed函数
incrementSpeed仅触发一次,但setSpeed可多次触发,已知该函数拿旧state的原因,但不知如何修复。
实际场景(第三版)
上述为简化示例,实际是开发浏览器扩展,包含两个功能:
- 自动滚动
- 调整滚动速度
自动滚动函数autoRead代码如下:
const autoRead = () => { if (stopAutoReading.current) { window.requestAnimationFrame(autoRead); return; } recordScroll.current += config.speed / 100; document.documentElement.scrollTop = recordScroll.current; if (enableAutoReading.current) { window.requestAnimationFrame(autoRead); } };
按下'x'键触发autoRead,按下左右箭头调整滚动速度,enableAutoReading为控制函数执行的ref,同样遇到无法获取更新后speed值的问题。
解决方案
核心原因
函数组件中,incrementSpeed和autoRead这类被requestAnimationFrame反复调用的函数,会捕获组件渲染时的state值形成闭包。后续state更新触发组件重新渲染,但旧的函数实例依然持有旧的state引用,导致无法获取最新值。
针对示例代码的修复方法
方法1:使用useRef保存最新state
通过useRef创建引用,在state更新时同步更新该ref,让闭包中的函数随时获取最新state值:
const TestApp = () => { const [speed, setSpeed] = useState(1); const speedRef = useRef(speed); // 同步state到ref useEffect(() => { speedRef.current = speed; }, [speed]); const incrementSpeed = () => { console.log(speedRef.current, `speed`); // 现在能获取最新值 setSpeed(prev => prev + 1); window.requestAnimationFrame(incrementSpeed); }; useEffect(() => { const handleKeydown = (e) => { if (e.key === 'z') { incrementSpeed(); } }; window.addEventListener('keydown', handleKeydown); return () => window.removeEventListener('keydown', handleKeydown); }, []); return <>></>; };
方法2:使用useCallback配合依赖更新函数
用useCallback包裹函数并加入speed依赖,同时注意管理requestAnimationFrame的请求ID,避免重复执行:
const TestApp = () => { const [speed, setSpeed] = useState(1); const requestIdRef = useRef(null); const incrementSpeed = useCallback(() => { console.log(speed, `speed`); setSpeed(prev => prev + 1); requestIdRef.current = window.requestAnimationFrame(incrementSpeed); }, [speed]); useEffect(() => { const handleKeydown = (e) => { if (e.key === 'z') { // 先取消之前的请求,避免重复执行 if (requestIdRef.current) { window.cancelAnimationFrame(requestIdRef.current); } incrementSpeed(); } }; window.addEventListener('keydown', handleKeydown); return () => { window.removeEventListener('keydown', handleKeydown); if (requestIdRef.current) { window.cancelAnimationFrame(requestIdRef.current); } }; }, [incrementSpeed]); return <>></>; };
针对实际浏览器扩展场景的修复
将config.speed替换为ref保存的最新值即可:
const TestApp = () => { const [speed, setSpeed] = useState(1); const speedRef = useRef(speed); const stopAutoReading = useRef(false); const recordScroll = useRef(0); const enableAutoReading = useRef(false); useEffect(() => { speedRef.current = speed; }, [speed]); const autoRead = () => { if (stopAutoReading.current) { window.requestAnimationFrame(autoRead); return; } // 使用ref中的最新speed值 recordScroll.current += speedRef.current / 100; document.documentElement.scrollTop = recordScroll.current; if (enableAutoReading.current) { window.requestAnimationFrame(autoRead); } }; useEffect(() => { const handleKeydown = (e) => { if (e.key === 'x') { enableAutoReading.current = true; autoRead(); } else if (e.key === 'ArrowLeft') { setSpeed(prev => Math.max(1, prev - 1)); } else if (e.key === 'ArrowRight') { setSpeed(prev => prev + 1); } }; window.addEventListener('keydown', handleKeydown); return () => window.removeEventListener('keydown', handleKeydown); }, []); return <>></>; };
内容的提问来源于stack exchange,提问作者Eve-Sama
相关产品推荐
相关产品推荐

