元件之間的狀態是隔離的。React 會根據元件在 UI 樹狀結構中的位置來追蹤哪些狀態屬於哪個元件。您可以控制在重新渲染之間何時保存狀態以及何時重置狀態。
您將學習
- React 何時選擇保存或重置狀態
- 如何強制 React 重置元件的狀態
- 鍵和類型如何影響狀態是否被保存
狀態與渲染樹中的位置相關聯
React 為 UI 中的元件結構構建 渲染樹。
當您賦予元件狀態時,您可能會認為狀態「存在於」元件內部。但實際上,狀態是保存在 React 內部的。React 會根據元件在渲染樹中的位置,將其持有的每個狀態片段與正確的元件關聯起來。
這裡只有一個 <Counter />
JSX 標籤,但它在兩個不同的位置渲染
import { useState } from 'react'; export default function App() { const counter = <Counter />; return ( <div> {counter} {counter} </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
以下是它們作為樹狀結構的樣子


React 樹
這是兩個獨立的計數器,因為每個計數器都在樹狀結構中自己的位置渲染。 您通常不需要考慮這些位置來使用 React,但了解其工作原理可能很有用。
在 React 中,螢幕上的每個元件都具有完全隔離的狀態。例如,如果您並排渲染兩個 Counter
元件,則每個元件都將獲得其自身的、獨立的 score
和 hover
狀態。
嘗試點擊兩個計數器,並注意它們不會互相影響
import { useState } from 'react'; export default function App() { return ( <div> <Counter /> <Counter /> </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
如您所見,當一個計數器更新時,只有該元件的狀態會更新


更新狀態
只要您在樹狀結構中的相同位置渲染相同的元件,React 就會保留該狀態。要查看這一點,請增加兩個計數器的值,然後取消勾選「渲染第二個計數器」核取方塊以移除第二個元件,然後再次勾選它以將其新增回來
import { useState } from 'react'; export default function App() { const [showB, setShowB] = useState(true); return ( <div> <Counter /> {showB && <Counter />} <label> <input type="checkbox" checked={showB} onChange={e => { setShowB(e.target.checked) }} /> Render the second counter </label> </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
請注意,當您停止渲染第二個計數器時,其狀態會完全消失。這是因為當 React 移除元件時,它會銷毀其狀態。


刪除元件
當您勾選「渲染第二個計數器」時,第二個 Counter
及其狀態會從頭開始初始化 (score = 0
) 並新增到 DOM 中。


新增元件
只要元件在其 UI 樹狀結構中的位置被渲染,React 就會保存其狀態。 如果它被移除,或者在相同位置渲染了不同的元件,React 就會丟棄其狀態。
相同位置的相同元件會保存狀態
在此範例中,有兩個不同的 <Counter />
標籤
import { useState } from 'react'; export default function App() { const [isFancy, setIsFancy] = useState(false); return ( <div> {isFancy ? ( <Counter isFancy={true} /> ) : ( <Counter isFancy={false} /> )} <label> <input type="checkbox" checked={isFancy} onChange={e => { setIsFancy(e.target.checked) }} /> Use fancy styling </label> </div> ); } function Counter({ isFancy }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } if (isFancy) { className += ' fancy'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
當您勾選或清除核取方塊時,計數器狀態不會重置。無論 isFancy
是 true
還是 false
,您始終在根 App
元件返回的 div
的第一個子節點中有一個 <Counter />


更新 App
狀態不會重置 Counter
,因為 Counter
保持在相同位置
它是相同位置的相同元件,因此從 React 的角度來看,它是同一個計數器。
相同位置的不同元件會重置狀態
在此範例中,勾選核取方塊會將 `<Counter>
` 替換為 `<p>
`
import { useState } from 'react'; export default function App() { const [isPaused, setIsPaused] = useState(false); return ( <div> {isPaused ? ( <p>See you later!</p> ) : ( <Counter /> )} <label> <input type="checkbox" checked={isPaused} onChange={e => { setIsPaused(e.target.checked) }} /> Take a break </label> </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
在這裡,您在相同位置切換*不同*的元件類型。最初,`<div>
` 的第一個子元件包含一個 `Counter
`。但是當您換成 `p
` 時,React 會從 UI 樹狀結構中移除 `Counter
` 並銷毀其狀態。


當 `Counter
` 更改為 `p
` 時,`Counter
` 會被刪除,而 `p
` 會被新增


切換回來時,`p
` 會被刪除,而 `Counter
` 會被新增
此外,**當您在相同位置渲染不同的元件時,它會重置其整個子樹狀結構的狀態。** 若要查看其運作方式,請增加計數器,然後勾選核取方塊
import { useState } from 'react'; export default function App() { const [isFancy, setIsFancy] = useState(false); return ( <div> {isFancy ? ( <div> <Counter isFancy={true} /> </div> ) : ( <section> <Counter isFancy={false} /> </section> )} <label> <input type="checkbox" checked={isFancy} onChange={e => { setIsFancy(e.target.checked) }} /> Use fancy styling </label> </div> ); } function Counter({ isFancy }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } if (isFancy) { className += ' fancy'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
當您點擊核取方塊時,計數器狀態會被重置。雖然您渲染了一個 `Counter
`,但 `div
` 的第一個子元件會從 `div
` 更改為 `section
`。當子 `div
` 從 DOM 中移除時,它下面的整個樹狀結構(包括 `Counter
` 及其狀態)也會被銷毀。


當 `section
` 更改為 `div
` 時,`section
` 會被刪除,而新的 `div
` 會被新增


切換回來時,`div
` 會被刪除,而新的 `section
` 會被新增
根據經驗法則,**如果您想在重新渲染之間保留狀態,您的樹狀結構必須在**一次渲染到下一次渲染之間「匹配」。如果結構不同,狀態就會被銷毀,因為 React 在從樹狀結構中移除元件時會銷毀狀態。
在相同位置重置狀態
預設情況下,React 會在元件保持在相同位置時保留其狀態。通常,這正是您想要的,因此將其設為預設行為是有道理的。但是有時候,您可能想要重置元件的狀態。請考慮這個應用程式,它允許兩位玩家在每個回合中追蹤他們的分數
import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA ? ( <Counter person="Taylor" /> ) : ( <Counter person="Sarah" /> )} <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Next player! </button> </div> ); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{person}'s score: {score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
目前,當您更改玩家時,分數會被保留。這兩個 `Counter
` 出現在相同位置,因此 React 將它們視為*相同*的 `Counter
`,其 `person
` 屬性已更改。
但在概念上,在此應用程式中,它們應該是兩個獨立的計數器。它們可能出現在 UI 中的同一個位置,但一個是 Taylor 的計數器,另一個是 Sarah 的計數器。
在它們之間切換時,有兩種方法可以重置狀態
- 在不同位置渲染元件
- 使用 `
key
` 為每個元件指定明確的識別
選項 1:在不同位置渲染元件
如果您希望這兩個 `Counter
` 獨立,您可以將它們渲染在兩個不同的位置
import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA && <Counter person="Taylor" /> } {!isPlayerA && <Counter person="Sarah" /> } <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Next player! </button> </div> ); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{person}'s score: {score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
- 最初,`
isPlayerA
` 為 `true
`。因此,第一個位置包含 `Counter
` 狀態,而第二個位置為空。 - 當您點擊「下一位玩家」按鈕時,第一個位置會清除,但第二個位置現在包含一個 `
Counter
`。


初始狀態


點擊「下一位」


再次點擊「下一位」
每次從 DOM 中移除時,每個 `Counter
` 的狀態都會被銷毀。這就是為什麼每次您點擊按鈕時它們都會重置的原因。
當您只在同一個位置渲染少數幾個獨立元件時,此解決方案很方便。在此範例中,您只有兩個,因此在 JSX 中分別渲染它們並不麻煩。
選項 2:使用 key 重置狀態
還有另一種更通用的方法可以重置元件的狀態。
您可能在渲染列表時看過 key
。Keys 不僅僅適用於列表!您可以使用 keys 來讓 React 區分任何元件。預設情況下,React 使用父元件中的順序(「第一個計數器」、「第二個計數器」)來區分元件。但 keys 讓您可以告訴 React 這不僅僅是第一個計數器或第二個計數器,而是一個特定的計數器,例如Taylor 的計數器。這樣,React 就會知道Taylor 的計數器在樹狀結構中的任何位置!
在此範例中,兩個 <Counter />
並未共享狀態,即使它們出現在 JSX 中的相同位置
import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA ? ( <Counter key="Taylor" person="Taylor" /> ) : ( <Counter key="Sarah" person="Sarah" /> )} <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Next player! </button> </div> ); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{person}'s score: {score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
在 Taylor 和 Sarah 之間切換不會保留狀態。這是因為您給了它們不同的 key
:
{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}
指定 key
會告訴 React 使用 key
本身作為位置的一部分,而不是它們在父元件中的順序。這就是為什麼即使您在 JSX 中的相同位置渲染它們,React 也會將它們視為兩個不同的計數器,因此它們永遠不會共享狀態。每次計數器出現在螢幕上時,都會建立其狀態。每次移除它時,它的狀態都會被銷毀。在它們之間切換會不斷重置它們的狀態。
使用 key 重置表單
使用 key 重置狀態在處理表單時特別有用。
在此聊天應用程式中,<Chat>
元件包含文字輸入狀態
import { useState } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; export default function Messenger() { const [to, setTo] = useState(contacts[0]); return ( <div> <ContactList contacts={contacts} selectedContact={to} onSelect={contact => setTo(contact)} /> <Chat contact={to} /> </div> ) } const contacts = [ { id: 0, name: 'Taylor', email: 'taylor@mail.com' }, { id: 1, name: 'Alice', email: 'alice@mail.com' }, { id: 2, name: 'Bob', email: 'bob@mail.com' } ];
嘗試在輸入框中輸入一些內容,然後按下「Alice」或「Bob」來選擇不同的收件者。您會注意到輸入狀態被保留,因為 <Chat>
在樹狀結構中的相同位置被渲染。
在許多應用程式中,這可能是預期的行為,但在聊天應用程式中則不然! 您不希望因為使用者意外點擊而讓他們將已經輸入的訊息發送給錯誤的人。要解決此問題,請新增一個 key
<Chat key={to.id} contact={to} />
這可確保當您選擇不同的收件者時,Chat
元件將從頭開始重新建立,包括其下方樹狀結構中的任何狀態。React 也會重新建立 DOM 元素,而不是重複使用它們。
現在切換收件者總是會清除文字欄位
import { useState } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; export default function Messenger() { const [to, setTo] = useState(contacts[0]); return ( <div> <ContactList contacts={contacts} selectedContact={to} onSelect={contact => setTo(contact)} /> <Chat key={to.id} contact={to} /> </div> ) } const contacts = [ { id: 0, name: 'Taylor', email: 'taylor@mail.com' }, { id: 1, name: 'Alice', email: 'alice@mail.com' }, { id: 2, name: 'Bob', email: 'bob@mail.com' } ];
深入探討
在真正的聊天應用程式中,當使用者再次選擇先前的收件者時,您可能希望恢復輸入狀態。有幾種方法可以讓不再可見的元件保持狀態「活動」
- 您可以渲染所有聊天,而不僅僅是目前的聊天,但使用 CSS 隱藏所有其他聊天。聊天不會從樹狀結構中移除,因此它們的本地狀態將被保留。此解決方案非常適合簡單的 UI。但如果隱藏的樹狀結構很大且包含許多 DOM 節點,它可能會變得非常慢。
- 您可以將狀態提升並在父元件中保存每個收件者的待處理訊息。這樣,當子元件被移除時,就無所謂了,因為是父元件保留了重要資訊。這是最常見的解決方案。
- 除了 React 狀態之外,您還可以改用不同的來源。例如,即使使用者意外關閉頁面,您可能也希望訊息草稿能夠保留。要實現這一點,您可以讓
Chat
元件透過從localStorage
讀取來初始化其狀態,並將草稿也儲存到那裡。
無論您選擇哪種策略,與Alice 的聊天在概念上都與與Bob 的聊天不同,因此根據目前的收件者為 <Chat>
樹狀結構提供一個 key
是有意義的。
重點回顧
- 只要相同的元件在相同位置渲染,React 就會保留狀態。
- 狀態不會保存在 JSX 標籤中。它與您放置該 JSX 的樹狀結構位置相關聯。
- 您可以透過賦予子樹不同的 key 來強制其重置狀態。
- 不要巢狀元件定義,否則您會意外重置狀態。
挑戰 1之 5: 修復消失的輸入文字
這個範例在按下按鈕時會顯示訊息。然而,按下按鈕也會意外地重置輸入。為什麼會發生這種情況?請修復它,讓按下按鈕不會重置輸入文字。
import { useState } from 'react'; export default function App() { const [showHint, setShowHint] = useState(false); if (showHint) { return ( <div> <p><i>Hint: Your favorite city?</i></p> <Form /> <button onClick={() => { setShowHint(false); }}>Hide hint</button> </div> ); } return ( <div> <Form /> <button onClick={() => { setShowHint(true); }}>Show hint</button> </div> ); } function Form() { const [text, setText] = useState(''); return ( <textarea value={text} onChange={e => setText(e.target.value)} /> ); }