You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 06:05:04