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 了,那是它在默默守护你的代码质量。

相关推荐
C_心欲无痕2 分钟前
网络相关 - Ngrok内网穿透使用
运维·前端·网络
咖啡の猫3 分钟前
TypeScript-Babel
前端·javascript·typescript
Mintopia29 分钟前
🤖 AI 决策 + 意图OS:未来软件形态的灵魂共舞
前端·人工智能·react native
攀登的牵牛花30 分钟前
前端向架构突围系列 - 框架设计(四):依赖倒置原则(DIP)
前端·架构
程序员爱钓鱼37 分钟前
Node.js 编程实战:测试与调试 —— 日志与监控方案
前端·后端·node.js
Mapmost1 小时前
数字孪生项目效率翻倍!AI技术实测与场景验证实录
前端
小酒星小杜1 小时前
在AI时代,技术人应该每天都要花两小时来构建一个自身的构建系统-Input篇
前端·程序员·架构
Cache技术分享1 小时前
290. Java Stream API - 从文本文件的行创建 Stream
前端·后端
陈_杨1 小时前
前端成功转鸿蒙开发者真实案例,教大家如何开发鸿蒙APP--ArkTS 卡片开发完全指南
前端·harmonyos