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 会:
- 第一次执行:检测到 timer
- 立即模拟卸载 → timer 变为 null(状态更新后);
- 再次挂载并执行 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 最佳实践、具备可维护性与鲁棒性的副作用逻辑。