<StrictMode> 是一个 React 组件,它会为其内部的所有内容开启额外的检查,而且只在开发环境中生效。它会把每个组件渲染两次,在挂载时依次运行每个 Effect 的 setup、cleanup,再运行一次 setup,把 ref 回调运行两次,并对已废弃的 API 发出警告。这就是 useEffect 在开发环境中会“执行两次”的原因。在生产构建中,它什么也不做。
开启方法是用它包裹你的应用,通常写在 main.jsx 里:
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
);
本页的预览以生产构建的方式运行,所以 StrictMode 在其中不会有可见的效果。下面的每个示例都包含一个 StrictMode 专门要抓的那类 bug,并让你手动触发它。这里的 Effect 给 document 添加了一个点击监听器,却从不移除。把面板隐藏、显示几次,然后点击页面。
每点一次 Show 就多一个监听器,而且一个都没被移除,所以点击一次,挂载过几次就会打印几行。这是用户只要打开、关闭面板就能造成的泄漏。StrictMode 在第一次挂载时就能把它暴露出来:在开发环境中它会挂载、卸载、再挂载,所以你还没碰按钮,每次点击就会看到两行日志。
Effect:setup、cleanup、setup
挂载时,StrictMode 会先运行你的 Effect,再运行它的清理函数,然后再运行一次 Effect。它是在模拟用户离开某个页面又回来时的情形,并检查你的清理函数是否撤销了 setup 所做的事。如果撤销了,第二次 setup 留下的状态就和只运行一次时完全一样,你不会发现任何问题。如果没有撤销,bug 会立刻显现出来。
修复这个日志组件的方法,是写一个清理函数,移除当初添加的同一个函数:
随便切换多少次都可以:日志组件显示时,每次点击只打印一行。在开发环境的 StrictMode 下,控制台开头会是 setup、cleanup、setup,而应用的行为依然正确。这个顺序是 Effect 健康的标志,不是需要关掉的问题。
常见的需要清理的 Effect 有:事件监听器、setInterval 和 setTimeout、订阅、WebSocket 或聊天连接,以及组件消失后就应该忽略其响应的请求。useEffect 页面为每一种都展示了清理的写法。
渲染:调用两次
StrictMode 还会在每次渲染时调用你的组件函数两次(传给 useState、useMemo 和 useReducer 的函数,以及类组件的 constructor、render 和 shouldComponentUpdate 也一样)。React 会保留其中一次调用的结果,丢弃另一次。纯组件两次返回的 JSX 完全相同,所以什么都不会改变。
在渲染时修改了自身以外东西的组件就不是纯的,调用两次会让这个修改发生两次。下面这个组件会往通过 prop 收到的数组里追加一条“rendered at”记录:
每次重新渲染都会多出一条没人要的记录。在这里,你要点击之后才会注意到。而在 StrictMode 下,第一次渲染就会多出两条新记录,而不是一条。修复方法是在渲染时计算,但不修改原数据:const shown = [...entries, 'extra']。
React 19 增加了一个细节:在双重渲染期间,useMemo 和 useCallback 会复用第一次调用的结果,而不是计算两次。
ref 回调
从 React 19 开始,StrictMode 还会在挂载时把 ref 回调运行两次:附加、清理、再附加。如果 ref 回调添加了监听器,或在某处登记了这个节点,就需要像 Effect 一样返回一个清理函数:
<div
ref={(node) => {
const observer = new ResizeObserver(onResize);
observer.observe(node);
return () => observer.disconnect();
}}
/>
已废弃 API 的警告
StrictMode 会对即将淘汰的 API 发出警告,这些 API 大多出现在类组件中,比如旧的 UNSAFE_componentWillMount、UNSAFE_componentWillReceiveProps 和 UNSAFE_componentWillUpdate 生命周期方法。它以前警告过的一些 API,比如字符串 ref 和 findDOMNode,已经在 React 19 中被彻底移除。
只为应用的一部分开启
<StrictMode> 是一个普通组件,所以你可以只包裹组件树的一部分。在大型应用中引入它时这很有用:先从新页面开始,再逐步扩大范围。
function App() {
return (
<>
<Header />
<StrictMode>
<Sidebar />
<Content />
</StrictMode>
<Footer />
</>
);
}
Sidebar 和 Content(以及它们渲染的所有内容)会受到检查;Header 和 Footer 不会。在严格模式的范围内,没有办法为某个子树单独关闭它。
默认开启它的地方:
- Vite:React 模板在
main.jsx中用<StrictMode>包裹了<App />。 - Next.js:App Router 默认开启;由
next.config.js中的reactStrictMode控制。
在生产环境中它什么也不做
生产构建会忽略 <StrictMode>:一次渲染,Effect 运行一次,没有警告。所以这些双重调用永远不会让你的用户付出任何代价,也没有理由在部署前移除这个包裹组件。
如果开发环境中 Effect 执行两次导致了问题,比如请求发送了两次,或者分析事件记录了两次,这本身就是一条有用的信息。大多数时候,修复方法是一个清理函数(请求用 ignore 标志,连接用断开)。对于每次加载应用真的只能执行一次的操作,比如初始化某个 SDK,就把它放在任何组件之外的模块顶层运行,或者用一个模块级的标志来保护它。
let didInit = false;
function App() {
useEffect(() => {
if (didInit) return;
didInit = true;
initAnalytics(); // runs once per page load, even in StrictMode
}, []);
}
不要用那个常见的变通办法:用 ref 跳过第二次 Effect 执行。它只是藏起了警告,并没有修好缺失的清理,等组件第一次真正卸载再挂载时,bug 就会回来,就像上面那个日志组件一样。
常见问题
React StrictMode 有什么作用?
它只在开发环境中生效:把其中的每个组件渲染两次,在组件挂载时依次运行每个 Effect 的 setup、cleanup,再运行一次 setup,以同样方式把 ref 回调运行两次,并对已废弃的 API 发出警告。这些额外的调用会暴露不纯的渲染和缺少清理的 Effect。
为什么我的 useEffect 执行了两次?
因为组件位于 <StrictMode> 之内,而你运行的是开发构建。React 会挂载它,模拟一次卸载(你的清理函数会运行),然后再挂载一次。如果第二次 setup 引发了 bug,说明这个 Effect 缺少清理。
StrictMode 会影响生产环境吗?
不会。生产构建无论有没有 <StrictMode>,都只渲染一次、每个 Effect 只运行一次。你可以放心地把它留在代码里。
如何关闭 StrictMode?
移除 <StrictMode> 包裹(通常在 main.jsx 或 index.js 里),或者在 next.config.js 中设置 reactStrictMode: false。不过修好清理函数通常是更好的做法:执行两次指出的是真实用户也可能遇到的 bug。
为什么 console.log 打印了两次?
在开发环境中,StrictMode 每次渲染都会调用你的组件函数两次。React DevTools 的设置里可以把第二次调用的日志显示为灰色,或者直接隐藏。