先给结论:React 严格模式建议一直开着。它只在开发环境生效,生产构建不会多跑一遍,作用是把两类原本要等上线才暴露的问题提前逼出来:副作用没有清理、渲染过程不纯。

很多新手第一次打开严格模式的反应是"代码明明没错,怎么日志打了两遍",顺手就把它关掉了。这是把报警当成了噪音。严格模式故意让组件多渲染一次、让 effect 多执行一轮,目的是让你在写完的十分钟内看到问题,而不是等用户在页面上点了几百次之后才发现内存里堆着一堆没取消的订阅。
本文基于 React 18 与 19 的行为说明,用三段可以复制运行的最小代码讲清:重复执行到底重复了什么、它真实会抓出哪些 bug、以及哪几种情况你会误以为它有问题。看完你能自己判断要不要开,也能把理由讲给团队里其他人听。
先看结论
- 严格模式不是编译开关,生产构建会被剥离,不会让线上变慢。
- 它重复执行的是渲染函数和 useEffect 的挂载清理流程,用来暴露没写清理逻辑的副作用。
- 看到重复日志先别关掉,先确认是不是自己的代码少了清理函数或者改了外部变量。
语法细节记不清的时候,翻 React 教程 里按主题分组的章节最快。
一、严格模式下到底重复了什么
严格模式是一个开发期组件,写法是把根组件包一层。
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>
);
这段做的是把 App 整个子树交给这层包装托管。生效范围只有开发环境,构建产物里这层包装不会带来额外渲染。
它做的三件事值得记住:
- 让组件的渲染函数执行一次以上,用来暴露渲染期间修改外部变量这类不纯写法;
- 让每个 effect 走一遍"挂载 → 清理 → 再挂载",用来暴露没写清理函数的问题;
- 对已被官方标记为废弃的写法给出控制台警告,比如旧版字符串 ref 和部分生命周期。
第三点是对新手最友好的一处:它相当于一个内置的升级提示器。真正容易让人困惑的是前两点,下面分开讲。
二、第一类坑:effect 的清理函数没写
这是严格模式最常抓出来的问题。假设有一个组件订阅了窗口尺寸变化,却忘了在卸载时取消订阅。
import { useEffect, useState } from 'react';
export default function Width() {
const [w, setW] = useState(0);
useEffect(() => {
const handler = () => setW(window.innerWidth);
window.addEventListener('resize', handler);
handler();
}, []);
return <p>窗口宽度 {w}</p>;
}
这段做的是在挂载时注册一个 resize 监听,然后立刻取一次宽度。问题在于没有返回清理函数。
预期行为:组件卸载后监听应该消失。实际行为:监听还在,组件被销毁后依然会调用 setW,在 React 18 之后的控制台里你会看到对已卸载组件更新的提示;如果这里换成定时器或者 WebSocket,表现就是定时器越堆越多、连接一直没断。
开启之后,这段代码会在挂载后立刻被清理再挂载一次,监听被注册两遍,重复触发的副作用在数据上直接翻倍,问题当场可见。
修复方式是补上清理函数:
useEffect(() => {
const handler = () => setW(window.innerWidth);
window.addEventListener('resize', handler);
handler();
return () => window.removeEventListener('resize', handler);
}, []);
这段做的是把注册与注销配成一对。加了清理之后,重复挂载两遍的结果和挂载一遍完全一致,这才是判断副作用写对了没有的标准:重复执行一次,结果必须和只执行一次相同。

三、第二类坑:渲染函数里改了外部变量
第二类问题是渲染不纯。下面的写法在开启这个模式后会立刻暴露。
let renderCount = 0;
export default function Counter() {
renderCount = renderCount + 1;
return <p>渲染了 {renderCount} 次</p>;
}
这段做的是用一个模块级变量统计渲染次数。它看起来"能用",但渲染函数改变了自己之外的状态,重复执行时结果就不一样了:明明只挂载了一次,页面上却显示 2。
同样的问题还包括在渲染期间往数组 push、修改传入的 props 对象、或者调用会改写外部缓存的函数。判断标准很简单:把这个组件的渲染函数连续调用两次,如果两次得到的东西不一样,它就是不纯的。
正确做法是把这类状态挪进 state 或者 ref,让它跟随组件实例而不是模块。
import { useRef } from 'react';
export default function Counter() {
const count = useRef(0);
count.current = count.current + 1;
return <p>渲染了 {count.current} 次</p>;
}
这段做的是把计数挂在组件自己的 ref 上。即便这样写仍然会在开发环境显示 2,但它不再污染模块作用域,不会让同一个组件的两个实例互相影响。
适用边界要讲清:它能抓的是"副作用不干净"和"渲染不纯"这两类,跨组件的重复请求、父组件重渲染导致的重复调用它抓不到,那些要靠开发者工具的性能面板去看。
💡 小提示:想快速确认是不是严格模式引起的重复,最省事的办法是临时把 StrictMode 那层去掉再看一次。现象消失,说明问题在你自己的副作用或渲染逻辑里;现象不变,说明是别的逻辑在重复触发。
四、常见失败分支与验证清单
4.1 以为线上也会执行两遍
这是最常见的误解。严格模式的重复执行只在开发环境生效,用 Vite、Next.js 或 Create React App 打包出来的产物里没有这层逻辑。如果你在生产环境也看到了重复请求,那要查的是别的地方:请求是不是被父组件的重复渲染触发了,或者路由是否有双重挂载。
4.2 数据请求发了两次导致后端报错
还有一个真实场景:effect 里发请求,开发环境看到两条相同请求。这里要分清两件事——请求本身是否幂等,以及有没有做竞态处理。正确做法是加一个忽略标记,让后一次的响应覆盖前一次。
useEffect(() => {
let ignore = false;
fetch('/api/list')
.then(r => r.json())
.then(data => { if (!ignore) setList(data); });
return () => { ignore = true; };
}, []);
这段做的是用闭包变量丢弃过期响应。加了它之后,无论重复挂载几遍,最终落到 state 上的都是最后一次有效结果。
4.3 第三方库的 effect 没法改
如果重复执行的问题来自第三方组件,自己改不了源码,那就在自己这一层补齐清理,或者把该组件隔离到不受这层包装影响的子树之外。不要用关掉它的方式来解决,那只是把问题藏起来。
4.4 验证清单
- 检查:控制台里重复出现的日志来自哪个 effect
- 运行:临时去掉 StrictMode 包装,观察现象是否消失
- 预期:现象消失说明是自己代码的副作用问题,不是框架 bug
- 失败:去掉后仍然重复,说明是父组件重复渲染或路由重复挂载
- 修复:补齐清理函数,或把请求加上忽略过期响应的处理
- 验证:重新打开这层包装,确认重复两遍的结果与一遍完全一致
上面这些步骤做的是定位"重复"的来源并确认修复有效。预期结果(未在本机执行)是重复执行后页面状态与单次执行一致;如果不一致,说明副作用还没写干净。

总结
React 严格模式值得常开:它在开发环境用"重复执行"这个简单手段,逼出副作用没清理和渲染不纯这两类问题,代价只是多几条日志。
三条要点带走:
- 严格模式只在开发环境生效,生产构建不会因此变慢;
- effect 必须配清理函数,判断标准是重复挂载结果与单次一致;
- 渲染函数不能改外部变量,否则重复执行的结果就会漂移。
下一步建议给每个新项目的入口文件默认加上这层包装,并在团队里约定:看到重复日志先查自己的清理逻辑,再考虑别的可能。组件通信的基础写法可以对照 React 教程 里的示例过一遍。
常见问题
Q:严格模式为什么会让我的接口请求发两次?
A:开发环境里 effect 会挂载、清理、再挂载,所以请求确实会发两次。这是设计意图,用来暴露没做清理和竞态处理的代码。线上不会出现。
Q:严格模式能不能关掉?怎么关?
A:能关,把入口文件里的 StrictMode 包装去掉即可。但更建议保留,它带来的开发期收益远大于那点日志噪音。
Q:为什么线上没有重复请求,本地却有?
A:因为这种重复执行只在开发环境生效。如果线上也重复了,要查的是父组件重复渲染或路由重复挂载。
Q:严格模式会影响性能吗?
A:不会影响生产性能。额外的渲染和 effect 重跑只发生在开发构建里,打包后的产物不包含这部分逻辑。
Q:我的 effect 已经写了清理函数,还要管这个模式吗?
A:基本不用。写了正确清理函数的 effect,重复挂载两遍的结果和一遍相同,它不会再报出问题。
延伸学习
想把这套副作用规则练熟,可以按这个顺序来:
- 想系统补一遍组件与状态,Web前端入门扫盲课程 可以跟一遍;
- 读一篇相关笔记 邪修 React 10 连击,看看别人踩过的渲染坑;
- 想随手验证一段逻辑,用 在线代码实例 跑一遍最快。

TRAE-AI编程



