<StrictMode>
让你在开发早期就能找到组件中的常见错误。
<StrictMode>
<App />
</StrictMode>
参考
<StrictMode>
使用 StrictMode
为内部组件树启用额外的开发行为和警告
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<App />
</StrictMode>
);
严格模式启用以下仅限开发的行为
- 你的组件将额外渲染一次,以查找由不纯渲染引起的错误。
- 你的组件将额外运行 Effects 一次,以查找由缺少 Effect 清理引起的错误。
- 你的组件将被检查是否使用了已弃用的 API。
属性
StrictMode
不接受任何属性。
注意事项
- 在
<StrictMode>
包装的树中,无法选择退出严格模式。这让你确信<StrictMode>
内部的所有组件都经过了检查。如果在产品上工作的两个团队对这些检查是否有价值存在分歧,他们需要达成共识,或者将<StrictMode>
移动到树的更下层。
用法
为整个应用程序启用严格模式
严格模式会在 <StrictMode> 组件内的整个组件树上启用额外的仅限开发环境的检查。这些检查可以帮助你在开发过程的早期发现组件中的常见错误。
要为整个应用程序启用严格模式,请在渲染根组件时使用 <StrictMode> 包裹它
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<App />
</StrictMode>
);
我们建议在严格模式下包裹你的整个应用程序,特别是对于新创建的应用程序。如果你使用的是为你调用 createRoot
的框架,请查看其文档以了解如何启用严格模式。
尽管严格模式检查**仅在开发环境中运行,**但它们可以帮助你发现代码中已经存在的错误,而这些错误在生产环境中可能难以可靠地重现。严格模式允许你在用户报告错误之前修复它们。
为应用程序的一部分启用严格模式
你也可以为应用程序的任何部分启用严格模式
import { StrictMode } from 'react';
function App() {
return (
<>
<Header />
<StrictMode>
<main>
<Sidebar />
<Content />
</main>
</StrictMode>
<Footer />
</>
);
}
在这个例子中,严格模式检查不会对 Header
和 Footer
组件运行。但是,它们会在 Sidebar
和 Content
上运行,以及它们内部的所有组件,无论嵌套多深。
修复在开发中通过双重渲染发现的错误
React 假设你编写的每个组件都是一个纯函数。这意味着你编写的 React 组件必须在给定相同的输入(属性、状态和上下文)时始终返回相同的 JSX。
违反此规则的组件会表现得不稳定并导致错误。为了帮助你发现意外的不纯代码,严格模式会在开发环境中**调用某些函数(仅限那些应该是纯函数的函数)两次。**这包括
- 你的组件函数体(仅限顶级逻辑,因此不包括事件处理程序内部的代码)
- 你传递给
useState
、set
函数、useMemo
或useReducer
的函数 - 某些类组件方法,例如
constructor
、render
、shouldComponentUpdate
(查看完整列表)
如果一个函数是纯函数,则运行两次不会改变其行为,因为纯函数每次都会产生相同的结果。但是,如果一个函数是不纯的(例如,它会改变它接收的数据),则运行两次往往会很明显(这就是它不纯的原因!)这可以帮助你在早期发现并修复错误。
下面是一个例子,说明严格模式下的双重渲染如何帮助你尽早发现错误。
此 StoryTray
组件接受一个 stories
数组,并在末尾添加一个“创建故事”项
export default function StoryTray({ stories }) { const items = stories; items.push({ id: 'create', label: 'Create Story' }); return ( <ul> {items.map(story => ( <li key={story.id}> {story.label} </li> ))} </ul> ); }
上面的代码中有一个错误。但是,它很容易被忽略,因为初始输出看起来是正确的。
如果 StoryTray
组件多次重新渲染,则此错误将变得更加明显。例如,让我们在将鼠标悬停在 StoryTray
上时,使用不同的背景颜色重新渲染它
import { useState } from 'react'; export default function StoryTray({ stories }) { const [isHover, setIsHover] = useState(false); const items = stories; items.push({ id: 'create', label: 'Create Story' }); return ( <ul onPointerEnter={() => setIsHover(true)} onPointerLeave={() => setIsHover(false)} style={{ backgroundColor: isHover ? '#ddd' : '#fff' }} > {items.map(story => ( <li key={story.id}> {story.label} </li> ))} </ul> ); }
请注意,每次将鼠标悬停在 StoryTray
组件上时,“创建故事”都会再次添加到列表中。代码的意图是在最后添加一次。但是 StoryTray
直接修改了 props 中的 stories
数组。每次 StoryTray
渲染时,它都会在同一个数组的末尾再次添加“创建故事”。换句话说,StoryTray
不是纯函数——多次运行它会产生不同的结果。
要解决此问题,你可以创建一个数组的副本,并修改该副本而不是原始副本
export default function StoryTray({ stories }) {
const items = stories.slice(); // Clone the array
// ✅ Good: Pushing into a new array
items.push({ id: 'create', label: 'Create Story' });
这将 使 StoryTray
函数成为纯函数。每次调用它时,它只会修改数组的新副本,而不会影响任何外部对象或变量。这解决了这个错误,但你必须让组件更频繁地重新渲染,然后才能发现它的行为有问题。
在最初的例子中,这个错误并不明显。现在让我们将原始(有错误的)代码包装在 <StrictMode>
中
export default function StoryTray({ stories }) { const items = stories; items.push({ id: 'create', label: 'Create Story' }); return ( <ul> {items.map(story => ( <li key={story.id}> {story.label} </li> ))} </ul> ); }
**严格模式*总是*调用你的渲染函数两次,因此你可以立即看到错误**(“创建故事”出现两次)。这使你能够在流程的早期发现此类错误。当你修复组件以在严格模式下渲染时,你*也*修复了许多可能的未来生产错误,例如之前的悬停功能
import { useState } from 'react'; export default function StoryTray({ stories }) { const [isHover, setIsHover] = useState(false); const items = stories.slice(); // Clone the array items.push({ id: 'create', label: 'Create Story' }); return ( <ul onPointerEnter={() => setIsHover(true)} onPointerLeave={() => setIsHover(false)} style={{ backgroundColor: isHover ? '#ddd' : '#fff' }} > {items.map(story => ( <li key={story.id}> {story.label} </li> ))} </ul> ); }
如果没有严格模式,你很容易错过这个错误,直到你添加了更多的重新渲染。严格模式使相同的错误立即出现。严格模式可帮助你在将错误推送到你的团队和用户之前找到它们。
修复在开发过程中重新运行 Effects 发现的错误
严格模式还可以帮助查找 Effects 中的错误。
每个 Effect 都有一些设置代码,并且可能还有一些清理代码。通常,React 在组件*挂载*(添加到屏幕)时调用设置,并在组件*卸载*(从屏幕中移除)时调用清理。如果自上次渲染以来其依赖项发生了更改,React 还会再次调用清理和设置。
当严格模式开启时,React 还会在开发过程中为每个 Effect 运行**一个额外的设置+清理周期**。这可能会让人感到惊讶,但它有助于揭示难以手动捕获的细微错误。
以下示例说明了在严格模式下重新运行 Effects 如何帮助您及早发现错误。
考虑这个将组件连接到聊天室的示例
import { createRoot } from 'react-dom/client'; import './styles.css'; import App from './App'; const root = createRoot(document.getElementById("root")); root.render(<App />);
这段代码存在一个问题,但可能不是立即就能看出来。
为了使问题更明显,让我们实现一个功能。在下面的示例中,roomId
不是硬编码的。相反,用户可以从下拉列表中选择他们想要连接到的 roomId
。点击“打开聊天室”,然后依次选择不同的聊天室。跟踪控制台中活动连接的数量
import { createRoot } from 'react-dom/client'; import './styles.css'; import App from './App'; const root = createRoot(document.getElementById("root")); root.render(<App />);
您会注意到打开的连接数量一直在增长。在真实的应用程序中,这会导致性能和网络问题。问题在于 您的 Effect 缺少清理函数:
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
现在,您的 Effect 会在自身之后“清理”并销毁过时的连接,从而解决了泄漏问题。但是,请注意,在您添加更多功能(选择框)之前,问题并没有显现出来。
在最初的例子中,这个错误并不明显。现在让我们将原始(有错误的)代码包装在 <StrictMode>
中
import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import './styles.css'; import App from './App'; const root = createRoot(document.getElementById("root")); root.render( <StrictMode> <App /> </StrictMode> );
**使用严格模式,您可以立即看到存在问题**(活动连接的数量跃升至 2)。严格模式会为每个 Effect 运行一个额外的设置+清理周期。此 Effect 没有清理逻辑,因此它会创建一个额外的连接,但不会销毁它。这暗示您缺少一个清理函数。
严格模式可让您在流程早期就注意到此类错误。当您通过在严格模式下为 Effect 添加清理函数来修复它时,您*也*修复了许多未来可能出现的生产错误,例如之前的选择框
import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import './styles.css'; import App from './App'; const root = createRoot(document.getElementById("root")); root.render( <StrictMode> <App /> </StrictMode> );
请注意,控制台中活动连接计数如何不再持续增长。
如果没有严格模式,很容易就会错过您的 Effect 需要清理。通过在开发中为您的 Effect 运行*设置 → 清理 → 设置*而不是*设置*,严格模式使缺少的清理逻辑更加明显。
修复由严格模式启用的弃用警告
如果 <StrictMode>
树中的任何组件使用以下任何已弃用的 API,React 就会发出警告
findDOMNode
。 查看替代方案。UNSAFE_
类生命周期方法,例如UNSAFE_componentWillMount
。 查看替代方案。- 旧版上下文(
childContextTypes
、contextTypes
和getChildContext
)。 查看替代方案。 - 旧版字符串 ref(
this.refs
)。 查看替代方案。
这些 API 主要用于较旧的 类组件,因此它们很少出现在现代应用程序中。