React中定时器回调内使用state变量的异常问题排查
问题解决:定时器触发时无法获取最新State值
问题原因
你碰到的是React经典的闭包陷阱:
- 组件初始化时,
useEffect创建了定时器,此时定时器里的updateTimer函数捕获了当时的sendAnswer。 sendAnswer作为组件内的函数,每次组件重新渲染(比如输入框改变answer时)都会重新创建,新的sendAnswer能拿到最新的answer和timerTime,但定时器里还是用的初始化时的旧sendAnswer,自然只能拿到state的初始值。
解决方案
给你两种简单有效的解决方式:
方式一:用useRef保存最新的State和sendAnswer
通过ref存储最新的state和函数,ref的current属性是可变的,不会被闭包锁定旧值:
import { useState, useEffect, useRef } from 'react'; function GameRound() { const [answer, setAnswer] = useState(""); const [timerTime, setTimerTime] = useState(30); const canSendRef = useRef(true); // 保存最新的answer和timerTime const stateRef = useRef({ answer: "", timerTime: 30 }); // 每次state更新时同步到ref useEffect(() => { stateRef.current = { answer, timerTime }; }, [answer, timerTime]); const sendAnswer = async (autoSent) => { // 从ref里拿最新的state值 const { answer, timerTime } = stateRef.current; fetch("...endpoint...", { // ...其他请求配置 body: JSON.stringify({ "answer": answer, "ready": !autoSent, "timeRemaining": timerTime }) }); // ...其他逻辑 }; useEffect(() => { const updateTimer = () => { setTimerTime((oldTimerTime) => { if(oldTimerTime === 1) { if(canSendRef.current) sendAnswer(true) } return oldTimerTime - 1; }); }; const timerInterval = setInterval(updateTimer, 1000); // 清理定时器,避免内存泄漏 return () => clearInterval(timerInterval); }, [sharedEventSource]); return ( <> {/*...其他页面元素*/} <input type="text" value={answer} onInput={e => setAnswer(e.target.value)} /> <div onClick={() => sendAnswer(false)}>提交答案</div> {/*...其他页面元素*/} </> ); }
方式二:修正useEffect的依赖数组,同时清理定时器
把sendAnswer加入useEffect的依赖数组,用useCallback缓存函数,避免不必要的重渲染:
import { useState, useEffect, useCallback } from 'react'; function GameRound() { const [answer, setAnswer] = useState(""); const [timerTime, setTimerTime] = useState(30); const canSendRef = useRef(true); // 用useCallback缓存sendAnswer,依赖变化时才更新函数 const sendAnswer = useCallback(async (autoSent) => { fetch("...endpoint...", { // ...其他请求配置 body: JSON.stringify({ "answer": answer, "ready": !autoSent, "timeRemaining": timerTime }) }); // ...其他逻辑 }, [answer, timerTime]); useEffect(() => { const updateTimer = () => { setTimerTime((oldTimerTime) => { if(oldTimerTime === 1) { if(canSendRef.current) sendAnswer(true) } return oldTimerTime - 1; }); }; const timerInterval = setInterval(updateTimer, 1000); // 组件卸载或依赖变化时清理旧定时器 return () => clearInterval(timerInterval); }, [sharedEventSource, sendAnswer]); // 加入sendAnswer作为依赖 return ( <> {/*...其他页面元素*/} <input type="text" value={answer} onInput={e => setAnswer(e.target.value)} /> <div onClick={() => sendAnswer(false)}>提交答案</div> {/*...其他页面元素*/} </> ); }
注意点
- 两种方式都要在
useEffect的返回函数里清理定时器,防止组件卸载后定时器持续运行导致内存泄漏。 - 方式二中的
useCallback是为了避免answer或timerTime每次变化时,sendAnswer都重新创建,导致useEffect频繁触发(这是合理的触发,因为函数本身需要更新以获取最新state)。
内容的提问来源于stack exchange,提问作者Crih.exe
相关产品推荐
相关产品推荐

