React useState状态变量作用域问题:userControlled状态无法切换
问题分析与解决方案
核心问题:闭包陷阱导致状态值 stale
你的keydown事件监听器是在组件首次渲染时通过空依赖数组的useEffect添加的,此时keydownHandler捕获的是初始渲染时的userControlled状态(值为false)。后续组件重新渲染时,这个监听器不会更新,所以无论你怎么调用setUserControlled,handler里打印的始终是初始的false值,自然无法正确切换状态。
另外代码还有两个关键问题:
useRef使用错误:直接给animationTimeout变量赋值,而非修改它的current属性- 未清理定时器:切换手动控制或组件卸载时,旧定时器仍在运行,会导致动画异常
修正步骤
1. 修复事件监听器的闭包问题
用useCallback包裹keydownHandler,并在添加监听器的useEffect中加入依赖,同时在组件卸载时移除监听器避免内存泄漏:
const keydownHandler = useCallback((event) => { console.log(event.keyCode); console.log("userControlled is", userControlled); if(event.keyCode === 13) { setUserControlled(prev => !prev); // 函数式更新,避免依赖问题 } }, [userControlled]) useEffect(() => { document.addEventListener("keydown", keydownHandler); return () => { document.removeEventListener("keydown", keydownHandler); }; }, [keydownHandler])
2. 正确使用useRef存储定时器
useRef创建的是可变容器,必须通过current属性访问和修改其值:
const animationTimeout = useRef(null); // 用const声明,不要用var
3. 清理定时器,避免异常动画
切换userControlled为true时清除运行中的定时器;同时在监听animationStep的useEffect中,每次设置新定时器前先清除旧的:
// 监听userControlled变化,重置步骤并清除定时器 useEffect(() => { setAnimationStep(0); if (animationTimeout.current) { clearTimeout(animationTimeout.current); animationTimeout.current = null; } }, [userControlled]) useEffect(() => { if (!userControlled) { // 先清除旧定时器 if (animationTimeout.current) { clearTimeout(animationTimeout.current); } animationTimeout.current = setTimeout(() => { goNextStep() }, 1500) } }, [animationStep, userControlled]) // 加入userControlled作为依赖
4. 修复goNextStep的闭包问题
使用函数式更新获取最新状态,避免闭包捕获旧值:
const goNextStep = () => { setAnimationStep(prevStep => { return (prevStep > 3) ? 0 : prevStep + 1; }); }
完整修正代码
import React, { useState, useEffect, useRef, useCallback } from 'react' const ThingToAnimate = (props) => { const [userControlled, setUserControlled] = useState(false) const [animationStep, setAnimationStep] = useState(4) const animationTimeout = useRef(null); useEffect(() => { goNextStep() }, []) const keydownHandler = useCallback((event) => { console.log(event.keyCode); console.log("userControlled is", userControlled); if(event.keyCode === 13) { setUserControlled(prev => !prev); } }, [userControlled]) useEffect(() => { document.addEventListener("keydown", keydownHandler); return () => { document.removeEventListener("keydown", keydownHandler); }; }, [keydownHandler]) useEffect(() => { setAnimationStep(0); if (animationTimeout.current) { clearTimeout(animationTimeout.current); animationTimeout.current = null; } }, [userControlled]) const goNextStep = () => { setAnimationStep(prevStep => { return (prevStep > 3) ? 0 : prevStep + 1; }); } useEffect(() => { if (!userControlled) { if (animationTimeout.current) { clearTimeout(animationTimeout.current); } animationTimeout.current = setTimeout(() => { goNextStep() }, 1500) } }, [animationStep, userControlled]) return ( // render code omitted; render is based on the current value of animationStep ) }
内容的提问来源于stack exchange,提问作者Jason FB
相关产品推荐
相关产品推荐

