
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 < 1 && timer != null 为真 → 清理定时器、置空 timer;
- 立即模拟卸载 → timer 变为 null(状态更新后);
- 再次挂载并执行 effect → 此时 timer 是 null,但 null < 1 为 false,而 timer != null 也为 false,整个条件为 false —— 本应不执行。
⚠️ 但你加了 && timer != null 后反而“触发两次”,真实原因在于:null < 1 在 JavaScript 中返回 true(因为 null 被强制转为 0,0 < 1 为真),所以 null < 1 && null != null 实际等于 true && false → false;而 0 < 1 && 0 != null 是 true && true → true。因此,当 timer 是 0 时条件成立;当 Strict Mode 强制重渲染导致 timer 短暂变为 null 后又恢复为 0(或因状态队列顺序引发竞态),可能造成两次满足条件的时机。
更根本的修复方式不是删条件,而是正确建模倒计时生命周期:
✅ 推荐写法(健壮、可读、规避 Strict Mode 干扰):
'use client';
import { useState, useEffect, useRef } from 'react';
export default function Home() {
const [timer, setTimer] = useState<number | null>(null);
const intervalRef = useRef<NodeJS.Timeout | null>(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 (
<main className="p-4">
<button onClick={start} disabled={timer !== null}>
{timer === null ? 'Start Timer' : `Remaining: ${timer}s`}
</button>
</main>
);
}? 关键改进点:
- 将 setInterval 和清理逻辑合并到单个 effect 中,并利用 return cleanup 确保资源释放;
- 使用函数式更新 setTimer(prev => ...) 避免闭包旧值问题;
- timer === null 作为完成标识,语义清晰且无类型歧义(避免 null < 1 的隐式转换陷阱);
- 启动按钮禁用逻辑提升用户体验。
? 补充说明:
- 若需临时关闭 Strict Mode(不推荐长期使用),可在 app/layout.tsx 或 src/index.js 中移除 <React.StrictMode> 包裹;
- 生产构建(next build && next start)自动禁用 Strict Mode,因此该现象仅存在于开发环境,不影响线上行为。
遵循上述模式,你不仅能解决“执行两次”的困惑,更能写出符合 React 最佳实践、具备可维护性与鲁棒性的副作用逻辑。








