React 中 useEffect 在开发模式下触发两次的原因及解决方案

react 开发模式下启用 strict mode 会导致 useeffect 模拟卸载/重挂载,从而执行两次;这是设计行为而非 bug,旨在帮助发现副作用清理问题。生产构建中不会出现此现象。

在 React(尤其是使用 App Router 的 Next.js 13+)中,你观察到 useEffect “执行两次”——例如倒计时结束时的清理逻辑被连续打印两次日志——这通常并非代码逻辑错误,而是 Strict Mode 的预期行为

Strict Mode 是 React 提供的开发辅助工具,它会在开发环境(npm run dev)中对组件进行双渲染(double-invocation):即先挂载、渲染、执行副作用,再立即模拟卸载并重新挂载、渲染、再次执行副作用。其核心目标是提前暴露未正确清理的副作用(如未清除的定时器、未解绑的事件监听器、未取消的网络请求等)。

在你的倒计时示例中:

useEffect(() => {
  if (timer < 1 && timer != null) {
    setTimer(null);
    clearInterval(intervalId.current);
    console.log("proccccccccccccc"); // 这里被打印两次
  }
}, [timer]);

当 timer 降为 0 时,该 effect 触发。由于 Strict Mode 的双渲染机制,React 会:

  1. 第一次执行:检测到 timer
  2. 立即模拟卸载 → timer 变为 null(状态更新后);
  3. 再次挂载并执行 effect → 此时 timer 是 null,但 null 本应不执行。

⚠️ 但你加了 && timer != null 后反而“触发两次”,真实原因在于:null (因为 null 被强制转为 0,0

更根本的修复方式不是删条件,而是正确建模倒计时生命周期

✅ 推荐写法(健壮、可读、规避 Strict Mode 干扰):

'use client';
import { useState, useEffect, useRef } from 'react';

export default function Home() {
  const [timer, setTimer] = useState(null);
  const intervalRef = useRef(null);

  const start = () => {
    setTimer(900);
  };

  // 启动定时器(仅在 timer 首次设为 900 时)
  useEffect(() => {
    if (timer === 900) {
      intervalRef.current = setInterval(() => {
        setTimer(prev => {
          if (prev === null || prev <= 1) {
            return null;
          }
          return prev - 1;
        });
      }, 1000);
    }

    // 清理函数:确保任何情况下都清除定时器
    return () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
      }
    };
  }, [timer]);

  // 倒计时结束处理(只在 timer 变为 null 时触发一次)
  useEffect(() => {
    if (timer === null) {
      console.log('Countdown finished!');
      // 执行完成逻辑(如弹窗、跳转等)
    }
  }, [timer]);

  return (
    
); }

? 关键改进点:

  • 将 setInterval 和清理逻辑合并到单个 effect 中,并利用 return cleanup 确保资源释放;
  • 使用函数式更新 setTimer(prev => ...) 避免闭包旧值问题;
  • timer === null 作为完成标识,语义清晰且无类型歧义(避免 null 隐式转换陷阱);
  • 启动按钮禁用逻辑提升用户体验。

? 补充说明:

  • 若需临时关闭 Strict Mode(不推荐长期使用),可在 app/layout.tsx 或 src/index.js 中移除 包裹;
  • 生产构建(next build && next start)自动禁用 Strict Mode,因此该现象仅存在于开发环境,不影响线上行为。

遵循上述模式,你不仅能解决“执行两次”的困惑,更能写出符合 React 最佳实践、具备可维护性与鲁棒性的副作用逻辑。