React 渲染两次:是 Bug 还是 Feature?聊聊严格模式的“良苦用心”

React 为啥老是渲染两次?------聊聊 Strict Mode 的那些事

看控制台的时候,有没有怀疑人生?

写 React 的时候,你有没有遇到过这种场景:

明明只写了一行 console.log,结果控制台"刷刷"给你印出来两条一模一样的。或者发送网络请求,明明只调用了一次,Network 里却躺着两个请求。

第一反应通常是:"完了,我是不是哪里写出 Bug 了?组件是不是在哪里被意外卸载又挂载了?"

别慌,大概率不是你的锅,而是 React 故意的。

罪魁祸首:Strict Mode

赶紧去你的入口文件(通常是 main.tsxindex.tsx)看一眼,是不是长这样:

tsx 复制代码
ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

那个 <React.StrictMode> 就是"幕后黑手"。

它的中文名叫"严格模式"。这玩意儿只在 开发环境(Development) 下生效,到了 生产环境(Production) 就会自动隐身,不会对用户产生任何影响。

为什么要搞这么个"恶作剧"?

React 团队并不是闲着没事干,非要让你的控制台变脏。这么做的核心目的是:帮你揪出不纯的函数和有副作用的代码

在 React 的设计哲学里,组件的渲染过程(Render Phase)应该是 纯粹(Pure) 的。

所谓的"纯",就是说:

  1. 给定相同的输入(Props 和 State),必须返回相同的输出(JSX)。
  2. 不能改变作用域之外的变量,不能有副作用(Side Effects)。

如果你的组件不纯,比如你在渲染函数里偷偷修改了一个全局变量:

tsx 复制代码
let count = 0;

function BadComponent() {
  count++; // ❌ 这是一个副作用!
  return <div>Count: {count}</div>;
}

在单次渲染下,你可能看不出问题。但如果 React 决定并发渲染、或者为了优化跳过某些渲染,这个全局 count 就会变得不可预测。

为了让你在开发阶段就发现这种隐患,React 采取了最简单粗暴的办法:把你的组件渲染两次

如果你的组件是纯的,渲染一次和渲染两次,对外部世界的影响应该是一样的(零影响),返回的结果也是一致的。但如果你在里面搞了小动作(比如上面的 count++),两次渲染就会导致 count 加了 2,结果就不对劲了,你立马就能发现问题。

具体哪些东西会执行两次?

在严格模式下,React 会特意重复调用以下内容:

  • 函数组件体(Function Component body)
  • useState, useMemo, useReducer 传递的初始化函数
  • 类组件的 constructor, render, shouldComponentUpdate 等生命周期

注意,这仅仅是 "调用" 两次,并不是把你的组件在 DOM 上真的挂载两次。它主要是在内存里跑两遍逻辑,看看有没有奇奇怪怪的副作用发生。

useEffect 的"挂载 -> 卸载 -> 挂载"

除了渲染过程,从 React 18 开始,Strict Mode 还加了一个更狠的检查机制,针对 useEffect

你可能会发现,组件初始化时,useEffect 里的代码也跑了两次。

严格来说,它的执行顺序是这样的:

  1. Mount(挂载) -> 执行 Effect
  2. Unmount(卸载) -> 执行 Cleanup(清除函数)
  3. Remount(挂载) -> 执行 Effect
graph LR A[组件挂载] --> B[执行 Effect] B --> C{严格模式?} C -- 是 --> D[模拟卸载: 执行 Cleanup] D --> E[再次挂载: 执行 Effect] C -- 否 --> F[结束]

这又是为了啥?

这是为了帮你检查 Cleanup 函数写没写对

很多时候我们写了订阅(subscribe),却忘了取消订阅(unsubscribe);写了 setInterval,却忘了 clearInterval。这种内存泄漏在单次挂载中很难发现,但在页面快速切换时就会爆雷。

通过强制来一次"挂载->卸载->挂载"的演习,React 逼着你必须把 Cleanup 逻辑写好。如果你的 Effect 写得没问题,那么"执行->清除->再执行"的结果,应该和"只执行一次"在逻辑上是闭环的。

比如一个聊天室连接:

  1. connect() (连接)
  2. disconnect() (断开)
  3. connect() (连接)

用户最终还是连接上了,中间的断开重连不应该导致程序崩溃或产生两个连接。

怎么解决?

1. 接受它,不要关掉它

最好的办法是适应它。既然 React 告诉你这里有副作用,那就去修复代码,而不是解决提出问题的人。

  • 把副作用挪到 useEffect 里去,别放在渲染函数体里。
  • 确保 useEffect 有正确的 Cleanup 函数。

2. 使用 useRef 解决数据重复请求

经常有人问:"我的请求在 useEffect 里发了两次,导致服务器存了两条数据,怎么办?"

如果你无法把后端接口改成幂等(Idempotent)的,可以使用 useRef 来标记请求状态:

tsx 复制代码
import { useEffect, useRef } from 'react';

function DataFetcher() {
  const hasFetched = useRef(false);

  useEffect(() => {
    if (hasFetched.current) return; // 如果已经请求过,直接返回

    hasFetched.current = true;
    fetchData();
  }, []);

  return <div>Loading...</div>;
}

不过 React 官方更推荐使用像 React Query (TanStack Query) 或 SWR 这样的库来管理数据请求,它们内部已经处理好了这些去重逻辑。

对于Strict Mode,我的理解是:

原理层面

  • 渲染双倍:为了检测渲染逻辑是否纯粹。
  • Effect 挂载-卸载-挂载:为了检测 Effect 的清除逻辑是否正确。
  • 仅限开发环境:生产环境完全无副作用。

实用层面

  • 它是 React 自带的"代码质量检测员"。
  • 看到日志打印两次不要慌,先想想是不是 Strict Mode 的锅。
  • 千万别在渲染函数里写副作用(比如修改外部变量、直接发请求)。

使用建议

  1. 调试时:如果在排查 Bug,可以留意一下是不是因为两次渲染导致的逻辑错误。
  2. 写 Effect 时:脑子里模拟一下"连上-断开-连上"的过程,看看代码能不能扛得住。
  3. 请求处理:尽量用成熟的请求库(React Query/SWR),或者确保接口幂等。

写在最后

Strict Mode 就像一个严格的健身教练,刚开始你会觉得它很烦,总是挑你的刺,让你做重复动作。但长远来看,它能帮你练就一身"健壮"的代码体格,避免在未来复杂的并发渲染中受内伤。

下次看到控制台的双重日志,别再骂 React 了,那是它在默默守护你的代码质量。

相关推荐
b***74882 小时前
前端GraphQL案例
前端·后端·graphql
云飞云共享云桌面2 小时前
无需配置传统电脑——智能装备工厂10个SolidWorks共享一台工作站
运维·服务器·前端·网络·算法·电脑
ganshenml2 小时前
sed 流编辑器在前端部署中的作用
前端·编辑器
0***K8923 小时前
Vue数据挖掘开发
前端·javascript·vue.js
蓝胖子的多啦A梦3 小时前
ElementUI表格错位修复技巧
前端·css·vue.js·el-table表格错位
QQRRRRW3 小时前
Tailwind+VScode (Vite + React + TypeScript) 原理与实践
vscode·react.js·typescript
_OP_CHEN3 小时前
前端开发实战深度解析:(一)认识前端和 HTML 与开发环境的搭建
前端·vscode·html·web开发·前端开发
xiAo_Ju4 小时前
iOS一个Fancy UI的Tricky实现
前端·ios
H***99764 小时前
Vue深度学习实战
前端·javascript·vue.js