你有没有注意到 React 组件在开发模式下被渲染了两次?

如果你是 React 开发者,在使用开发服务器(比如通过 npm start 运行的 Create React App)时,你可能会不止一次地注意到一个“奇怪”的现象:你的某些组件似乎被渲染了两次。尤其是在控制台中,如果你在组件内部放置了 console.log 语句,它们会输出两次。这可能会让你感到困惑,甚至开始怀疑自己的代码是不是出了什么问题。别担心!这不仅是正常的,而且是 React 为了你的代码健康而特意设计的,它背后的“功臣”就是 React.StrictMode

什么是 React StrictMode?

React.StrictMode(严格模式)是 React 提供的一个开发辅助工具,它不会在生产环境中运行,只在开发模式下对你的应用执行一系列额外的检查和警告。它的主要目的是帮助你识别潜在的问题,确保你的应用程序在未来版本的 React 中能够正常工作,并遵循最佳实践。
你可以在应用的根组件(通常是 App 组件)或者任何你想要启用严格检查的部分,将它们用 <React.StrictMode> 包裹起来:
JSX
import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <App /> </React.StrictMode> );
启用严格模式后,它会做以下几件事:
  1. 识别具有不安全生命周期的方法: 警告使用旧版、可能导致问题的生命周期方法。
  2. 警告使用已弃用的 API: 比如旧版的字符串 ref API。
  3. 检测意外的副作用: 这是与我们今天讨论的“双重渲染”最密切相关的一点。
  4. 警告使用旧版上下文 API。
  5. 确保 useStateuseMemouseReducer 的初始化函数和更新函数是纯净的。

为什么 StrictMode 要“两次渲染”组件?

现在我们来到核心问题了:为什么严格模式要让组件渲染两次?简单来说,这是一种**“压力测试”或者“干跑(dry run)”**。React 严格模式会故意调用某些函数两次,包括组件的构造函数、渲染函数,以及某些 effect hook 的回调和清理函数。它模拟了组件在未来可能遇到的快速卸载、重新挂载、或者异步更新的场景。
想象一下,你的代码就像一个房间里的水管系统。严格模式就像一个经验丰富的管道工,他会故意把某个水龙头快速地打开又关上,或者先打开一个水龙头,再迅速打开另一个,来检查整个系统是否有任何潜在的“跑冒滴漏”或者不稳定之处。如果你的水管系统在这些快速操作下表现正常,那么在常规使用时就更不会有问题了。
具体到 React,这种双重渲染是为了帮助你发现组件中不纯净的副作用。在 React 中,组件的渲染函数(或者函数组件的整个主体)应该是一个纯函数。这意味着它应该只根据 props 和 state 计算输出,而不应该修改外部变量,或者执行网络请求、订阅事件等有副作用的操作。如果你的渲染函数做了这些不纯净的事情,那么当它被调用两次时,这些副作用也会发生两次,从而暴露出来。

双重渲染主要帮助你检测什么?

  1. 不纯净的渲染函数: 如果你在组件渲染期间(例如,直接在函数组件的顶层或者 render 方法中)执行了数据修改、网络请求、或者订阅事件,那么双重渲染会导致这些操作重复执行。这很可能不是你想要的结果,并且会引起难以预料的 bug。
    JSX
    function MyComponent() { // 🚨 错误示例:直接在渲染期间进行副作用操作 // 这会在开发模式下执行两次,可能导致重复的数据请求或其他问题 // fetchData(); console.log('组件渲染了!'); return <div>Hello</div>; }
  2. 不当的 useEffect 清理: useEffect 是处理副作用的正确地方。但如果你的 useEffect 设置了订阅、定时器或者其他需要清理的资源,而没有提供一个正确的清理函数(即 return 一个函数),那么当组件被快速卸载又挂载时,这些资源就会堆积,导致内存泄漏或者行为异常。 严格模式通过“双重调用” useEffect 的清理函数和 effect 函数来验证你的清理逻辑。它会先运行一次 effect,然后立即运行一次清理函数,接着再运行一次 effect。这模拟了组件卸载并立即重新挂载的场景。
    JSX
    import React, { useEffect, useState } from 'react'; function TimerComponent() { const [count, setCount] = useState(0); useEffect(() => { console.log('useEffect 运行了!'); const interval = setInterval(() => { setCount(prevCount => prevCount + 1); }, 1000); return () => { console.log('useEffect 清理了!'); clearInterval(interval); // ✅ 正确的清理 }; }, []); return <div>计时器: {count}</div>; }
    如果你忘记了 clearInterval(interval),在严格模式下你就会看到两个计时器同时运行,或者行为异常,从而提醒你缺少清理。
  3. useStateuseReducer 的初始化器与更新器: 它们也应该保持纯净和幂等。严格模式会尝试运行两次这些函数来检查它们是否会产生意外的副作用。

生产环境中也会双重渲染吗?

绝对不会!React.StrictMode 只在开发模式下生效。在你的应用部署到生产环境时,所有严格模式的检查都会被移除,组件将按照正常逻辑只渲染一次(除非有状态或 props 的变化导致重新渲染)。它是一个纯粹的开发辅助工具,不会对用户体验和应用性能造成任何影响。

我该如何处理双重渲染带来的“问题”?

当你看到双重渲染导致一些意想不到的行为时,这正是严格模式在发挥作用!你应该做的不是禁用严格模式,而是修复你的代码,使其能够适应这种“压力测试”
  • 确保渲染函数是纯净的: 任何修改状态、发起网络请求、订阅事件等副作用都应该放在 useEffect 中。
  • 提供正确的 useEffect 清理函数: 如果你的 useEffect 创建了需要销毁或取消的资源,务必在 return 的函数中进行清理。
  • 利用 useRef 存储不应该触发重新渲染的持久化值: 如果某个值在多次渲染之间需要保持不变,但又不属于组件的状态,useRef 是一个好选择。
通过遵循这些原则,你的组件将变得更加健壮、可预测,并且能够更好地适应 React 未来的更新(例如并发模式的特性)。

总结

所以,当你下次在开发模式下看到 React 组件被渲染了两次时,请不要惊慌!这正是 React.StrictMode 在默默地为你工作,帮助你编写出更健壮、更可靠的 React 应用。把它看作是你开发过程中的一个忠实伙伴,它会提前帮你发现那些隐藏的、可能会在生产环境中引起麻烦的问题。拥抱严格模式,让你的 React 代码更上一层楼吧!