<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 清理引起的错误。
- 你的组件将额外重新运行 refs 回调一次,以查找由缺少 ref 清理引起的错误。
- 你的组件将被检查是否使用了已弃用的 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 组件必须始终在给定相同输入(props、状态和上下文)的情况下返回相同的 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
函数成为纯函数。每次调用它时,它只会修改数组的新副本,而不会影响任何外部对象或变量。这解决了bug,但是你必须让组件更频繁地重新渲染才能看出其行为有什么问题。
在原始示例中,这个bug并不明显。现在让我们将原始(有bug的)代码包装在<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> ); }
在没有严格模式的情况下,很容易错过这个bug,直到你添加更多重新渲染。严格模式使同样的bug立即显现。严格模式可以帮助你尽早发现bug,避免将其提交给你的团队和用户。
修复在开发过程中重新运行 Effects 发现的bug
严格模式还可以帮助发现Effects中的bug。
每个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“清理”自身并销毁过时的连接,泄漏问题就解决了。但是,请注意,这个问题直到你添加更多功能(选择框)后才变得可见。
在原始示例中,这个bug并不明显。现在让我们将原始(有bug的)代码包装在<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运行设置→清理→设置而不是设置,严格模式使缺失的清理逻辑更易于察觉。
修复在开发过程中重新运行ref回调发现的bug
严格模式还可以帮助发现回调ref中的bug。
每个回调ref
都有一些设置代码,也可能有一些清理代码。通常,React会在元素创建(添加到DOM)时调用设置,并在元素移除(从DOM移除)时调用清理。
启用严格模式后,React还会在开发环境中为每个回调ref运行一个额外的设置+清理周期。这可能令人惊讶,但它有助于发现难以手动捕获的细微错误。
考虑这个示例,它允许你选择一种动物,然后滚动到其中一种动物。注意,当你从“猫”切换到“狗”时,控制台日志显示列表中的动物数量不断增加,“滚动到”按钮停止工作。
import { useRef, useState } from "react"; export default function AnimalFriends() { const itemsRef = useRef([]); const [animalList, setAnimalList] = useState(setupAnimalList); const [animal, setAnimal] = useState('cat'); function scrollToAnimal(index) { const list = itemsRef.current; const {node} = list[index]; node.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center", }); } const animals = animalList.filter(a => a.type === animal) return ( <> <nav> <button onClick={() => setAnimal('cat')}>Cats</button> <button onClick={() => setAnimal('dog')}>Dogs</button> </nav> <hr /> <nav> <span>Scroll to:</span>{animals.map((animal, index) => ( <button key={animal.src} onClick={() => scrollToAnimal(index)}> {index} </button> ))} </nav> <div> <ul> {animals.map((animal) => ( <li key={animal.src} ref={(node) => { const list = itemsRef.current; const item = {animal: animal, node}; list.push(item); console.log(`✅ Adding animal to the map. Total animals: ${list.length}`); if (list.length > 10) { console.log('❌ Too many animals in the list!'); } return () => { // 🚩 No cleanup, this is a bug! } }} > <img src={animal.src} /> </li> ))} </ul> </div> </> ); } function setupAnimalList() { const animalList = []; for (let i = 0; i < 10; i++) { animalList.push({type: 'cat', src: "https://loremflickr.com/320/240/cat?lock=" + i}); } for (let i = 0; i < 10; i++) { animalList.push({type: 'dog', src: "https://loremflickr.com/320/240/dog?lock=" + i}); } return animalList; }
这是一个生产环境bug!由于ref回调在清理时没有从列表中删除动物,因此动物列表不断增长。这是一个内存泄漏,会在真实的应用程序中导致性能问题,并破坏应用程序的行为。
问题在于ref回调没有自行清理。
<li
ref={node => {
const list = itemsRef.current;
const item = {animal, node};
list.push(item);
return () => {
// 🚩 No cleanup, this is a bug!
}
}}
</li>
现在让我们将原始(有bug的)代码包装在<StrictMode>
中。
import { useRef, useState } from "react"; export default function AnimalFriends() { const itemsRef = useRef([]); const [animalList, setAnimalList] = useState(setupAnimalList); const [animal, setAnimal] = useState('cat'); function scrollToAnimal(index) { const list = itemsRef.current; const {node} = list[index]; node.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center", }); } const animals = animalList.filter(a => a.type === animal) return ( <> <nav> <button onClick={() => setAnimal('cat')}>Cats</button> <button onClick={() => setAnimal('dog')}>Dogs</button> </nav> <hr /> <nav> <span>Scroll to:</span>{animals.map((animal, index) => ( <button key={animal.src} onClick={() => scrollToAnimal(index)}> {index} </button> ))} </nav> <div> <ul> {animals.map((animal) => ( <li key={animal.src} ref={(node) => { const list = itemsRef.current; const item = {animal: animal, node} list.push(item); console.log(`✅ Adding animal to the map. Total animals: ${list.length}`); if (list.length > 10) { console.log('❌ Too many animals in the list!'); } return () => { // 🚩 No cleanup, this is a bug! } }} > <img src={animal.src} /> </li> ))} </ul> </div> </> ); } function setupAnimalList() { const animalList = []; for (let i = 0; i < 10; i++) { animalList.push({type: 'cat', src: "https://loremflickr.com/320/240/cat?lock=" + i}); } for (let i = 0; i < 10; i++) { animalList.push({type: 'dog', src: "https://loremflickr.com/320/240/dog?lock=" + i}); } return animalList; }
使用严格模式,你会立即看到存在问题。严格模式为每个回调ref运行额外的设置+清理周期。此回调ref没有清理逻辑,因此它添加了ref但没有删除它们。这暗示你缺少清理函数。
严格模式使你能够及早发现回调ref中的错误。当你通过在严格模式下添加清理函数来修复回调时,你也修复了许多可能出现的未来生产错误,例如之前的“滚动到”bug。
import { useRef, useState } from "react"; export default function AnimalFriends() { const itemsRef = useRef([]); const [animalList, setAnimalList] = useState(setupAnimalList); const [animal, setAnimal] = useState('cat'); function scrollToAnimal(index) { const list = itemsRef.current; const {node} = list[index]; node.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center", }); } const animals = animalList.filter(a => a.type === animal) return ( <> <nav> <button onClick={() => setAnimal('cat')}>Cats</button> <button onClick={() => setAnimal('dog')}>Dogs</button> </nav> <hr /> <nav> <span>Scroll to:</span>{animals.map((animal, index) => ( <button key={animal.src} onClick={() => scrollToAnimal(index)}> {index} </button> ))} </nav> <div> <ul> {animals.map((animal) => ( <li key={animal.src} ref={(node) => { const list = itemsRef.current; const item = {animal, node}; list.push({animal: animal, node}); console.log(`✅ Adding animal to the map. Total animals: ${list.length}`); if (list.length > 10) { console.log('❌ Too many animals in the list!'); } return () => { list.splice(list.indexOf(item)); console.log(`❌ Removing animal from the map. Total animals: ${itemsRef.current.length}`); } }} > <img src={animal.src} /> </li> ))} </ul> </div> </> ); } function setupAnimalList() { const animalList = []; for (let i = 0; i < 10; i++) { animalList.push({type: 'cat', src: "https://loremflickr.com/320/240/cat?lock=" + i}); } for (let i = 0; i < 10; i++) { animalList.push({type: 'dog', src: "https://loremflickr.com/320/240/dog?lock=" + i}); } return animalList; }
现在在严格模式下的初始挂载中,ref回调都被设置、清理,然后再次设置。
...
✅ Adding animal to the map. Total animals: 10
...
❌ Removing animal from the map. Total animals: 0
...
✅ Adding animal to the map. Total animals: 10
这是预期的。严格模式确认ref回调已正确清理,因此大小永远不会超过预期数量。修复后,没有内存泄漏,所有功能都按预期工作。
在没有严格模式的情况下,很容易错过这个bug,直到你点击应用程序周围才能注意到功能损坏。严格模式使bug立即出现,在你将其推送到生产环境之前。
修复严格模式启用的弃用警告
如果`<StrictMode>
树中的任何组件使用以下任何已弃用的API,React将发出警告。
UNSAFE_
类生命周期方法,例如UNSAFE_componentWillMount
。查看替代方案。
这些API主要用于较旧的 类组件,因此它们很少出现在现代应用程序中。