如何避免useEffect中useState的setState触发重渲染?倒计时分钟同步问题
解决倒计时时钟的分钟同步与重渲染问题
问题根源
你当前的代码把兄弟组件count值同步和倒计时逻辑塞进了同一个useEffect,而且依赖项包含min和second——每次秒数变化都会触发这个useEffect,里面的setMin(count)会直接把倒计时中的分钟值重置为兄弟组件传入的count,导致分钟无法正常递减;如果删掉setMin(count),兄弟组件的count变化时又无法同步到min状态。
另外原代码还有个隐性问题:countdown里只用了单次setTimeout,倒计时只会执行一次,不会持续走时,这个需要改成setInterval或者递归调用setTimeout。
方案一:拆分useEffect(最简洁的解决方式)
把同步兄弟组件count的逻辑单独抽成一个useEffect,只在count变化时更新min;倒计时逻辑单独处理,避免互相干扰。
修改后的代码:
export default function Timer(props) { const { count } = props; const [isPlay, setIsPlay] = useState(false); const [isRestore, setIsRestore] = useState(false); const [second, setSecond] = useState(0); const [isBreak, setIsBreak] = useState(false); const [min, setMin] = useState(count); // 单独处理兄弟组件count的同步:只有count变化时才更新min useEffect(() => { setMin(count); }, [count]); // 单独处理倒计时逻辑 useEffect(() => { let alarm = document.getElementById("beep"); let timerId; const countdown = () => { if (second === 0 && min > 0) { setMin(prevMin => prevMin - 1); setSecond(59); } else if (min >= 0 && second > 0) { setSecond(prevSec => prevSec - 1); } else { alarm.play(); const handleEnd = () => { setIsBreak(prev => !prev); alarm.removeEventListener("ended", handleEnd); // 避免内存泄漏 }; alarm.addEventListener("ended", handleEnd); } }; if (isPlay) { // 用setInterval持续触发倒计时 timerId = setInterval(countdown, 1000); } else { alarm.pause(); if (timerId) clearInterval(timerId); // 停止定时器 } if (isRestore) { setIsPlay(false); alarm.currentTime = 0; setIsRestore(false); setMin(count); // 重置时同步最新的count setSecond(0); } // 清理函数:组件卸载或依赖变化时清除定时器和事件监听 return () => { if (timerId) clearInterval(timerId); alarm.removeEventListener("ended", () => {}); }; }, [isPlay, isRestore, min, second, count]); // ... 其他渲染逻辑 }
关键修改点:
- 拆分两个
useEffect,职责单一:一个专门同步count,一个专门处理倒计时 - 倒计时用
setInterval替代单次setTimeout,保证持续走时 - 更新状态时用函数式更新(
prevMin => prevMin -1),避免闭包导致拿不到最新状态值 - 添加清理函数,清除定时器和事件监听,防止内存泄漏
方案二:用useRef配合状态(解决闭包问题场景)
如果倒计时逻辑中需要频繁访问最新的分钟/秒值,避免闭包陷阱,可以用useRef保存最新的状态值,同时配合状态更新。
示例代码:
export default function Timer(props) { const { count } = props; const [isPlay, setIsPlay] = useState(false); const [isRestore, setIsRestore] = useState(false); const [second, setSecond] = useState(0); const [isBreak, setIsBreak] = useState(false); const [min, setMin] = useState(count); // 用ref保存最新的min和second值,避免闭包问题 const minRef = useRef(min); const secondRef = useRef(second); // 同步count到状态和ref useEffect(() => { setMin(count); minRef.current = count; }, [count]); // 状态更新时同步ref的值 useEffect(() => { minRef.current = min; secondRef.current = second; }, [min, second]); useEffect(() => { let alarm = document.getElementById("beep"); let timerId; const countdown = () => { const currentMin = minRef.current; const currentSec = secondRef.current; if (currentSec === 0 && currentMin > 0) { setMin(currentMin - 1); setSecond(59); } else if (currentMin >= 0 && currentSec > 0) { setSecond(currentSec - 1); } else { alarm.play(); const handleEnd = () => { setIsBreak(prev => !prev); alarm.removeEventListener("ended", handleEnd); }; alarm.addEventListener("ended", handleEnd); } }; if (isPlay) { timerId = setInterval(countdown, 1000); } else { alarm.pause(); if (timerId) clearInterval(timerId); } if (isRestore) { setIsPlay(false); alarm.currentTime = 0; setIsRestore(false); setMin(count); setSecond(0); } return () => { if (timerId) clearInterval(timerId); alarm.removeEventListener("ended", () => {}); }; }, [isPlay, isRestore, count]); // ... 其他渲染逻辑 }
关键说明:
minRef和secondRef始终保存最新的状态值,在countdown中直接访问ref.current就能拿到最新值,不用依赖状态变量- 单独用
useEffect同步状态到ref,保证ref和状态一致 - 倒计时逻辑的依赖项不再需要
min和second,减少不必要的重渲染
内容的提问来源于stack exchange,提问作者Sean Lau
相关产品推荐
相关产品推荐

