<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>
);
嚴格模式啟用以下僅限開發的行為
- 您的元件會額外渲染一次,以找出由不純粹渲染引起的錯誤。
- 您的元件會額外重新執行副作用一次,以找出由缺少副作用清除引起的錯誤。
- 您的元件會額外重新執行 ref 回呼一次,以找出由缺少 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 元件在給定相同的輸入(屬性、狀態和上下文)時,必須始終返回相同的 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
` 直接修改了來自屬性的 `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> ); }
嚴格模式 (Strict Mode) *永遠* 會呼叫您的渲染函式兩次,以便您立即發現錯誤(「建立故事」會出現兩次)。這讓您可以在流程早期就注意到此類錯誤。當您修復組件使其在嚴格模式下正確渲染時,您*也*修復了許多未來可能發生的生產環境錯誤,例如之前的懸停功能問題。
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 在組件*掛載*(新增到螢幕)時呼叫設定,並在組件*卸載*(從螢幕移除)時呼叫清除。如果 Effects 的依賴項自上次渲染以來發生變化,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 執行 *設定 → 清除 → 設定* 而不是 *設定*,嚴格模式讓缺少的清除邏輯更加明顯。
修復在開發過程中重新執行 ref 回呼所發現的錯誤
嚴格模式也可以幫助找出回呼 ref 中的錯誤。
每個回呼 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; }
這是一個生產環境錯誤!由於 ref 回呼沒有在清除時從列表中移除動物,因此動物列表持續增長。這是一個記憶體洩漏,會在實際應用程式中導致效能問題,並破壞應用程式的行為。
問題是 ref 回呼沒有自行清理。
<li
ref={node => {
const list = itemsRef.current;
const item = {animal, node};
list.push(item);
return () => {
// 🚩 No cleanup, this is a bug!
}
}}
</li>
現在讓我們將原始(錯誤的)程式碼包裝在 <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 中的錯誤。當您透過在嚴格模式下新增清除函式來修復回呼時,您*也*修復了許多未來可能發生的生產環境錯誤,例如之前的「滾動到」錯誤。
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 回呼已正確清除,因此大小永遠不會超過預期數量。修復後,沒有記憶體洩漏,所有功能都按預期工作。
如果沒有嚴格模式,在您點擊應用程式並注意到功能損壞之前,很容易錯過這個錯誤。嚴格模式會讓錯誤立即出現,在您將它們推送至生產環境之前。
修復由嚴格模式啟用的棄用警告
如果 <StrictMode>
樹狀結構中的任何組件使用其中一個已棄用的 API,React 就會發出警告。
UNSAFE_
類別生命週期方法,例如UNSAFE_componentWillMount
。請參閱替代方案。
這些 API 主要用於較舊的 類別組件 中,因此它們很少出現在現代應用程式中。