React函数组件中setInterval状态不更新原因及不使用useRef的解决方法
问题原因解释
React函数组件的每一次渲染都对应一个独立的作用域,本次渲染内声明的所有state、函数都属于这个作用域的快照。你在点击按钮调用tick启动setInterval的时候,回调函数捕获的是本次渲染作用域内的foodLevel、healthLevel等状态值,这些值在定时器运行的全程都不会更新,所以打印出来永远是初始值10。
你用函数式更新setFoodLevel(prev => prev -1)能正常更新页面,是因为React会把更新函数推入队列,执行时自动传入最新的状态值,不需要你自己访问当前状态,所以页面渲染是正常的。
不使用useRef的实现方案
下面两种方案都不需要为每个状态单独创建ref,完全符合React的设计范式:
方案1:用useEffect管理定时器,依赖项注入需要用到的状态
每次状态更新后,清除旧的定时器,用最新的状态重新启动定时器,这样新的定时器回调就能捕获到最新的状态值:
const Cat = () => { const [name, setName] = useState(null); const [foodLevel, setFoodLevel] = useState(10); const [healthLevel, setHealthLevel] = useState(10); const [warning, setWarning] = useState(''); const [isGameRunning, setIsGameRunning] = useState(false); const [value, setValue] = useState(''); const onChangeHandler = (event) => { setValue(event.target.value); }; const startClickHandler = () => { setName(value); setIsGameRunning(true); }; // 用useEffect管理定时器生命周期 useEffect(() => { if (!isGameRunning || healthLevel === 0) return; const intervalId = setInterval(() => { if (foodLevel > 0) { setFoodLevel(prev => prev - 1); console.log(foodLevel); // 可正常获取最新状态 } else { setHealthLevel(prev => prev - 1); console.log(healthLevel); // 可正常获取最新状态 } }, 1000); // 状态变化、组件卸载、游戏结束时自动清理定时器 return () => clearInterval(intervalId); }, [isGameRunning, foodLevel, healthLevel]); return(...) };
方案2:用setTimeout递归调度替代setInterval
不需要维护定时器id,每次调度的setTimeout回调都属于最新的渲染作用域,天然能拿到最新状态,相比固定间隔的setInterval,更适合这种依赖状态变化的定时任务:
const Cat = () => { const [name, setName] = useState(null); const [foodLevel, setFoodLevel] = useState(10); const [healthLevel, setHealthLevel] = useState(10); const [warning, setWarning] = useState(''); const [isGameRunning, setIsGameRunning] = useState(false); const [value, setValue] = useState(''); const onChangeHandler = (event) => { setValue(event.target.value); }; const startClickHandler = () => { setName(value); setIsGameRunning(true); }; useEffect(() => { if (!isGameRunning || healthLevel === 0) return; const timer = setTimeout(() => { if (foodLevel > 0) { setFoodLevel(prev => prev - 1); console.log(foodLevel); } else { setHealthLevel(prev => prev - 1); console.log(healthLevel); } }, 1000); return () => clearTimeout(timer); }, [isGameRunning, foodLevel, healthLevel]); return(...); };
内容的提问来源于stack exchange,提问作者Evgenii Komornyi
相关产品推荐
相关产品推荐

