React中reducer副作用处理规范及定时器组件优化方案
问题解答
核心疑问:Reducer必须是纯函数吗?
肯定的,React的useReducer明确要求reducer必须是纯函数,还要保证幂等性——相同的state+action输入,必须返回完全一致的state输出,绝对不能在reducer里做副作用操作(比如创建定时器、修改全局变量、发请求这类)。
你遇到的“启动时创建两个定时器”问题,就是因为开发模式下React会自动执行两次reducer来检测不纯操作,而你的原reducer里直接调用了startTicking创建定时器,导致重复执行,最终暂停时只能清理一个,功能失效。
符合React范式的DRY封装方案
用useEffect监听状态变化来处理副作用是最标准的做法,既能保持reducer的纯净,又能让副作用自动触发,不用手动写一堆重复的封装函数,完美解决维护风险问题:
优化后的完整代码
import { useReducer, useRef, useEffect } from 'react'; const initialTimerState = { isRunning: false, startTime: null as Date | null, elapsedTime: 0, // 这里可以保留你修复resume问题的额外状态,比如totalPausedTime: 0 }; type TimerAction = 'start' | 'pause' | 'resume' | 'stop' | 'tick'; // 纯函数Reducer:只做状态更新,半点儿副作用都不沾 const timerReducer = (state: typeof initialTimerState, action: TimerAction) => { switch (action) { case 'start': return { ...state, isRunning: true, startTime: new Date(), // totalPausedTime: 0, // 配合修复resume的状态重置 }; case 'pause': return { ...state, isRunning: false, // 这里可以计算暂停时长,更新totalPausedTime来修复resume计时问题 }; case 'resume': return { ...state, isRunning: true, startTime: new Date(), // 或者基于totalPausedTime计算新的有效startTime }; case 'stop': return { ...state, isRunning: false, startTime: null, elapsedTime: 0, // totalPausedTime: 0, }; case 'tick': if (!state.startTime) return state; return { ...state, elapsedTime: Math.round((new Date().getTime() - state.startTime.getTime()) / 1000), // 如果有totalPausedTime,这里要加上该值得到实际耗时 }; default: return state; } }; const TimerComponent = () => { const [timerState, timerDispatch] = useReducer(timerReducer, initialTimerState); const timerRef = useRef<number | null>(null); // 监听isRunning状态,自动启停定时器 useEffect(() => { if (timerState.isRunning) { timerRef.current = window.setInterval(() => { timerDispatch('tick'); }, 1000); } else { if (timerRef.current) { window.clearInterval(timerRef.current); timerRef.current = null; } } // 组件卸载时务必清理定时器,避免内存泄漏 return () => { if (timerRef.current) { window.clearInterval(timerRef.current); } }; }, [timerState.isRunning]); // 只有isRunning变化时才执行这个effect return ( <div> <p>已耗时:{timerState.elapsedTime}秒</p> <button onClick={() => timerDispatch('start')} disabled={timerState.isRunning}> 开始 </button> <button onClick={() => timerDispatch('pause')} disabled={!timerState.isRunning}> 暂停 </button> <button onClick={() => timerDispatch('resume')} disabled={timerState.isRunning}> 恢复 </button> <button onClick={() => timerDispatch('stop')}> 停止 </button> </div> ); }; export default TimerComponent;
为啥这个方案好用?
- 合规性拉满:reducer彻底变成纯函数,完全符合React的设计要求,再也不会出现开发模式下重复执行的问题
- 副作用自动处理:通过
useEffect监听isRunning,状态一变自动启停定时器,不用写一堆startTimer/pauseTimer的重复代码 - 维护成本低:其他开发者只用调用
timerDispatch触发对应action就行,根本不用关心定时器怎么创建销毁,不会出现误用timerDispatch的情况 - 代码不冗余:所有副作用逻辑都集中在一个
useEffect里,改起来也方便
内容的提问来源于stack exchange,提问作者YakovL
相关产品推荐
相关产品推荐

