v19.2Latest

保留與重置狀態

狀態在元件之間是隔離的。React 會根據元件在 UI 樹中的位置來追蹤哪個狀態屬於哪個元件。您可以控制在重新渲染時何時保留狀態,何時重置狀態。

您將學習
  • React 何時選擇保留或重置狀態
  • 如何強制 React 重置元件的狀態
  • 鍵值和類型如何影響狀態是否被保留

狀態與渲染樹中的位置綁定

React 會為您 UI 中的元件結構建立渲染樹

當您為元件賦予狀態時,您可能會認為狀態「存在」於元件內部。但實際上狀態是由 React 持有的。React 會根據元件在渲染樹中的位置,將其持有的每個狀態片段與正確的元件關聯起來。

這裡只有一個<Counter />JSX 標籤,但它在兩個不同的位置被渲染:

它們在樹中的樣子如下:

React 元件樹的圖示。根節點標記為 'div',有兩個子節點。每個子節點都標記為 'Counter',並且都包含一個標記為 'count'、值為 0 的狀態氣泡。React 元件樹的圖示。根節點標記為 'div',有兩個子節點。每個子節點都標記為 'Counter',並且都包含一個標記為 'count'、值為 0 的狀態氣泡。

React 樹

這是兩個獨立的計數器,因為每個都在樹中的各自位置上被渲染。您通常不需要考慮這些位置來使用 React,但了解其工作原理可能會有幫助。

在 React 中,螢幕上的每個元件都有完全隔離的狀態。例如,如果您並排渲染兩個Counter元件,它們各自都會獲得自己獨立且互不影響的scorehover狀態。

試著點擊兩個計數器,並注意它們不會互相影響:

如您所見,當一個計數器被更新時,只有該元件的狀態會被更新:

React 元件樹的圖示。根節點標記為 'div',有兩個子節點。左子節點標記為 'Counter',包含一個標記為 'count'、值為 0 的狀態氣泡。右子節點標記為 'Counter',包含一個標記為 'count'、值為 1 的狀態氣泡。右子節點的狀態氣泡以黃色高亮顯示,表示其值已更新。React 元件樹的圖示。根節點標記為 'div',有兩個子節點。左子節點標記為 'Counter',包含一個標記為 'count'、值為 0 的狀態氣泡。右子節點標記為 'Counter',包含一個標記為 'count'、值為 1 的狀態氣泡。右子節點的狀態氣泡以黃色高亮顯示,表示其值已更新。

更新狀態

只要您在樹中的相同位置渲染相同的元件,React 就會保留該狀態。要觀察這一點,請先增加兩個計數器的值,然後透過取消勾選「渲染第二個計數器」核取方塊來移除第二個元件,接著再勾選它以重新添加:

請注意,當您停止渲染第二個計數器時,它的狀態會完全消失。這是因為當 React 移除一個元件時,它會銷毀其狀態。

React 元件樹的圖示。根節點標記為 'div',有兩個子節點。左子節點標記為 'Counter',包含一個標記為 'count'、值為 0 的狀態氣泡。右子節點缺失,其位置有一個黃色的 'poof' 圖像,高亮顯示該元件正從樹中被刪除。React 元件樹狀圖。根節點標記為 'div',有兩個子節點。左側子節點標記為 'Counter',包含一個標記為 'count' 的狀態泡泡,值為 0。右側子節點缺失,其位置有一個黃色的 '噗' 圖像,強調該元件正從樹中被刪除。

刪除一個元件

當你勾選「渲染第二個計數器」時,第二個Counter 及其狀態會從頭初始化(score = 0)並加入到 DOM 中。

React 元件樹狀圖。根節點標記為 'div',有兩個子節點。左側子節點標記為 'Counter',包含一個標記為 'count' 的狀態泡泡,值為 0。右側子節點標記為 'Counter',包含一個標記為 'count' 的狀態泡泡,值為 0。整個右側子節點以黃色高亮,表示它剛剛被加入到樹中。React 元件樹狀圖。根節點標記為 'div',有兩個子節點。左側子節點標記為 'Counter',包含一個標記為 'count' 的狀態泡泡,值為 0。右側子節點標記為 'Counter',包含一個標記為 'count' 的狀態泡泡,值為 0。整個右側子節點以黃色高亮,表示它剛剛被加入到樹中。

新增一個元件

只要元件在其 UI 樹中的位置被渲染,React 就會保留其狀態。如果它被移除,或者在同一個位置渲染了不同的元件,React 就會丟棄其狀態。

相同位置上的相同元件會保留狀態

在這個例子中,有兩個不同的<Counter />標籤:

當你勾選或清除核取方塊時,計數器狀態不會被重置。無論isFancytrue還是false,你始終有一個<Counter /> 作為根 App 元件返回的 div的第一個子元件:

圖表包含兩個由過渡箭頭分隔的區塊。每個區塊都包含一個元件佈局,其中有一個標記為'App'的父元件,內含一個標記為isFancy的狀態氣泡。此元件有一個標記為'div'的子元件,它連接到一個包含isFancy(以紫色高亮顯示)的屬性氣泡,並向下傳遞給唯一的子元件。最後一個子元件標記為'Counter',包含一個標記為'count'且值為3的狀態氣泡(兩個圖表中皆為3)。在圖表的左側區塊中,沒有任何內容被高亮,且isFancy父狀態值為false。在圖表的右側區塊中,isFancy父狀態值已變更為true並以黃色高亮顯示,其下方的屬性氣泡也同樣被高亮,其isFancy值也已變更為true。圖表包含兩個由過渡箭頭分隔的區塊。每個區塊都包含一個元件佈局,其中有一個標記為'App'的父元件,內含一個標記為isFancy的狀態氣泡。此元件有一個標記為'div'的子元件,它連接到一個包含isFancy(以紫色高亮顯示)的屬性氣泡,並向下傳遞給唯一的子元件。最後一個子元件標記為'Counter',包含一個標記為'count'且值為3的狀態氣泡(兩個圖表中皆為3)。在圖表的左側區塊中,沒有任何內容被高亮,且isFancy父狀態值為false。在圖表的右側區塊中,isFancy父狀態值已變更為true並以黃色高亮顯示,其下方的屬性氣泡也同樣被高亮,其isFancy值也已變更為true。

更新 App的狀態並不會重置Counter,因為Counter 保持在相同的位置

它是位於相同位置的相同元件,因此從 React 的角度來看,它是同一個計數器。

陷阱

請記住,對 React 來說重要的是 UI 樹中的位置,而不是 JSX 標記中的位置! 這個元件有兩個 return 子句,分別在 <Counter /> 條件內外包含不同的 ifJSX 標籤:

你可能會預期當你勾選核取方塊時狀態會重置,但並沒有!這是因為這兩個 <Counter /> 標籤都在相同位置渲染。React 不知道你在函數中放置條件的位置。它「看到」的只是你回傳的樹狀結構。

在這兩種情況下,App元件都會回傳一個<div>,其第一個子元素是<Counter />。對 React 來說,這兩個計數器擁有相同的「地址」:根元素的第一個子元素的第一個子元素。這就是 React 在先前和下一次渲染之間匹配它們的方式,無論你如何組織你的邏輯。

相同位置的不同元件會重置狀態

在這個範例中,勾選核取方塊會將 <Counter>替換為一個<p>

在這裡,你在同一個位置切換了不同的元件類型。最初,<div>的第一個子元素包含一個Counter。但當你換入一個p時,React 從 UI 樹中移除了Counter並銷毀了它的狀態。

包含三個部分的圖表,各部分之間有箭頭過渡。第一部分包含一個標記為'div'的 React 元件,其單個子元件標記為'Counter',其中包含一個標記為'count'、值為 3 的狀態泡泡。中間部分有相同的'div'父元件,但子元件現已被刪除,以一個黃色的'proof'圖示表示。第三部分再次有相同的'div'父元件,現在有一個標記為'p'的新子元件,以黃色高亮顯示。包含三個部分的圖表,各部分之間有箭頭過渡。第一部分包含一個標記為'div'的 React 元件,其單個子元件標記為'Counter',其中包含一個標記為'count'、值為 3 的狀態泡泡。中間部分有相同的'div'父元件,但子元件現已被刪除,以一個黃色的'proof'圖示表示。第三部分再次有相同的'div'父元件,現在有一個標記為'p'的新子元件,以黃色高亮顯示。

Counter變為p時,Counter被刪除,而p被新增

包含三個部分的圖表,各部分之間有箭頭過渡。第一部分包含一個標記為'p'的 React 元件。中間部分有相同的'div'父元件,但子元件現已被刪除,以一個黃色的'proof'圖示表示。第三部分再次有相同的'div'父元件,現在有一個標記為'Counter'的新子元件,其中包含一個標記為'count'、值為 0 的狀態泡泡,以黃色高亮顯示。包含三個區塊的圖表,每個區塊之間有箭頭表示轉換。第一個區塊包含一個標記為 'p' 的 React 元件。中間區塊有相同的 'div' 父元件,但子元件現已被刪除,以黃色的 'proof' 圖像表示。第三個區塊再次有相同的 'div' 父元件,現在有一個標記為 'Counter' 的新子元件,其中包含一個標記為 'count' 且值為 0 的狀態泡泡,並以黃色高亮顯示。

當切換回來時,p 被刪除,而 Counter被加入

此外,當你在相同位置渲染不同的元件時,它會重置其整個子樹的狀態。要了解其運作方式,請增加計數器然後勾選核取方塊:

當你點選核取方塊時,計數器狀態會被重置。雖然你渲染了一個Counter,但 div的第一個子元素從section變成了div。當子元素 section從 DOM 中被移除時,其下方的整個樹(包括Counter及其狀態)也隨之被銷毀。

包含三個區塊的圖表,每個區塊之間有箭頭表示轉換。第一個區塊包含一個標記為 'div' 的 React 元件,它有一個標記為 'section' 的子元件,該子元件又有一個標記為 'Counter' 的子元件,其中包含一個標記為 'count' 且值為 3 的狀態泡泡。中間區塊有相同的 'div' 父元件,但子元件現已被刪除,以黃色的 'proof' 圖像表示。第三個區塊再次有相同的 'div' 父元件,現在有一個標記為 'div' 的新子元件(以黃色高亮顯示),同樣也有一個標記為 'Counter' 的新子元件,其中包含一個標記為 'count' 且值為 0 的狀態泡泡,全部以黃色高亮顯示。圖表包含三個部分,各部分之間有箭頭表示轉換。第一部分包含一個標記為 'div' 的 React 元件,它有一個標記為 'section' 的子元件,該子元件又有一個標記為 'Counter' 的子元件,其中包含一個標記為 'count' 的狀態泡泡,值為 3。中間部分有相同的 'div' 父元件,但子元件現已被刪除,以黃色的 'proof' 圖像表示。第三部分再次有相同的 'div' 父元件,現在有一個新的標記為 'div' 的子元件(以黃色高亮顯示),還有一個新的標記為 'Counter' 的子元件,其中包含一個標記為 'count' 的狀態泡泡,值為 0,全部以黃色高亮顯示。

section 變更為 div時,section 會被刪除,新的 div會被加入

包含三個區塊的圖表,每個區塊之間有箭頭表示轉換。第一個區塊包含一個標記為'div'的 React 元件,它有一個標記為'div'的子元件,該子元件又有一個標記為'Counter'的子元件,其中包含一個標記為'count'、值為 0 的狀態泡泡。中間區塊有相同的'div'父元件,但子元件現已被刪除,以黃色的'proof'圖示表示。第三個區塊再次有相同的'div'父元件,現在有一個標記為'section'的新子元件(以黃色高亮顯示),還有一個標記為'Counter'的新子元件,其中包含一個標記為'count'、值為 0 的狀態泡泡(全部以黃色高亮顯示)。包含三個區塊的圖表,每個區塊之間有箭頭表示轉換。第一個區塊包含一個標記為'div'的 React 元件,它有一個標記為'div'的子元件,該子元件又有一個標記為'Counter'的子元件,其中包含一個標記為'count'、值為 0 的狀態泡泡。中間區塊有相同的'div'父元件,但子元件現已被刪除,以黃色的'proof'圖示表示。第三個區塊再次有相同的'div'父元件,現在有一個標記為'section'的新子元件(以黃色高亮顯示),還有一個標記為'Counter'的新子元件,其中包含一個標記為'count'、值為 0 的狀態泡泡(全部以黃色高亮顯示)。

當切換回來時,div 被刪除,新的 section被加入

經驗法則是,如果你想在重新渲染之間保留狀態,你的樹狀結構需要在每次渲染之間「匹配」。如果結構不同,狀態會被銷毀,因為當 React 從樹中移除一個元件時,它會銷毀該元件的狀態。

陷阱

這就是為什麼你不應該嵌套元件函數定義。

這裡,MyTextField 元件函數定義在 MyComponent內部MyComponent

每次你點擊按鈕時,輸入狀態就會消失!這是因為每次不同的MyTextField函數。你在相同位置渲染了一個MyComponent渲染時,都會創建一個不同的元件,因此 React 重置了其下的所有狀態。這會導致錯誤和效能問題。為避免此問題,請始終在頂層宣告元件函數,不要嵌套它們的定義。

在同一位置重置狀態

預設情況下,當元件保持在相同位置時,React 會保留其狀態。通常,這正是你想要的,因此將其作為預設行為是合理的。但有時,你可能希望重置元件的狀態。考慮這個讓兩位玩家在每回合中記錄分數的應用程式:

目前,當您切換玩家時,分數會被保留。兩個 Counter出現在相同的位置,因此 React 將它們視為同一個Counter,只是其person屬性發生了變化。

但從概念上講,在這個應用程式中,它們應該是兩個獨立的計數器。它們可能在 UI 中出現在相同的位置,但一個是 Taylor 的計數器,另一個是 Sarah 的計數器。

在兩者之間切換時,有兩種方法可以重置狀態:

  1. 在不同的位置渲染元件
  2. 使用 key 為每個元件提供明確的身份標識

選項 1:在不同的位置渲染元件

如果您希望這兩個Counter 彼此獨立,可以在兩個不同的位置渲染它們:

  • 最初,isPlayerAtrue。因此第一個位置包含Counter的狀態,而第二個位置是空的。
  • 當你點擊「下一位玩家」按鈕時,第一個位置會被清空,但第二個位置現在包含了一個Counter
React 元件樹狀圖。父元件標記為 'Scoreboard',帶有一個標記為 isPlayerA 且值為 'true' 的狀態泡泡。唯一的子元件(排列在左側)標記為 Counter,帶有一個標記為 'count' 且值為 0 的狀態泡泡。整個左側子元件以黃色高亮顯示,表示它被新增。React 元件樹狀圖。父元件標記為 'Scoreboard',帶有一個標記為 isPlayerA 且值為 'true' 的狀態泡泡。唯一的子元件(排列在左側)標記為 Counter,帶有一個標記為 'count' 且值為 0 的狀態泡泡。整個左側子元件以黃色高亮顯示,表示它被新增。

初始狀態

React 元件樹狀圖。父元件標記為 'Scoreboard',帶有一個標記為 isPlayerA 且值為 'false' 的狀態泡泡。該狀態泡泡以黃色高亮顯示,表示它已改變。左側子元件被一個黃色的 'poof' 圖像取代,表示它已被刪除,並且右側有一個新的子元件,以黃色高亮顯示表示它被新增。新的子元件標記為 'Counter',並包含一個標記為 'count' 且值為 0 的狀態泡泡。React 元件樹狀圖。父元件標記為 'Scoreboard',帶有一個標記為 isPlayerA 且值為 'false' 的狀態泡泡。該狀態泡泡以黃色高亮顯示,表示它已改變。左側子元件被一個黃色的 'poof' 圖像取代,表示它已被刪除,並且右側有一個新的子元件,以黃色高亮顯示表示它被新增。新的子元件標記為 'Counter',並包含一個標記為 'count' 且值為 0 的狀態泡泡。

點擊「下一位」

React 元件樹狀圖。父元件標記為 'Scoreboard',帶有一個標記為 isPlayerA 且值為 'true' 的狀態泡泡。該狀態泡泡以黃色高亮顯示,表示它已改變。左側有一個新的子元件,以黃色高亮顯示表示它被新增。新的子元件標記為 'Counter',並包含一個標記為 'count' 且值為 0 的狀態泡泡。右側子元件被一個黃色的 'poof' 圖像取代,表示它已被刪除。React 元件樹狀圖。父元件標記為 'Scoreboard',帶有一個標記為 isPlayerA 且值為 'true' 的狀態泡泡。該狀態泡泡以黃色高亮顯示,表示它已改變。左側有一個新的子元件,以黃色高亮顯示表示它被新增。新的子元件標記為 'Counter',並包含一個標記為 'count' 且值為 0 的狀態泡泡。右側子元件被一個黃色的 'poof' 圖像取代,表示它已被刪除。

再次點擊「下一位」

每個Counter的狀態在每次從 DOM 中移除時都會被銷毀。這就是為什麼每次點擊按鈕時它們都會重置。

當你只有少數幾個獨立元件渲染在同一個位置時,這個解決方案很方便。在這個例子中,你只有兩個元件,所以在 JSX 中分別渲染它們並不麻煩。

選項 2:使用 key 重置狀態

還有另一種更通用的方法來重置元件的狀態。

您可能在key渲染清單時見過鍵值不僅僅用於清單!您可以使用鍵值讓 React 區分任何元件。預設情況下,React 使用元件在父層中的順序(「第一個計數器」、「第二個計數器」)來辨識元件。但鍵值讓您可以告訴 React,這不僅僅是第一個計數器,或第二個計數器,而是一個特定的計數器——例如,Taylor 的計數器。這樣,無論Taylor 的計數器出現在樹狀結構中的哪個位置,React 都能識別它!

在這個例子中,兩個<Counter />元件不會共享狀態,即使它們在 JSX 中出現在相同的位置:

在 Taylor 和 Sarah 之間切換不會保留狀態。這是因為您為它們指定了不同的key

指定一個key會告訴 React 使用key本身作為位置的一部分,而不是它們在父層中的順序。這就是為什麼,即使您在 JSX 中的相同位置渲染它們,React 也會將它們視為兩個不同的計數器,因此它們永遠不會共享狀態。每次計數器出現在螢幕上時,其狀態都會被建立。每次它被移除時,其狀態都會被銷毀。在它們之間切換會反覆重置它們的狀態。

注意

請記住,鍵值並非全域唯一。它們僅指定在父層內的位置。

使用鍵值重置表單

使用鍵值重置狀態在處理表單時特別有用。

在這個聊天應用程式中,<Chat>元件包含文字輸入的狀態:

試著在輸入框中輸入一些內容,然後點擊「Alice」或「Bob」來選擇不同的收件人。你會注意到輸入狀態被保留了,因為<Chat>元件在樹狀結構中的相同位置被渲染。

在許多應用程式中,這可能是期望的行為,但在聊天應用程式中並非如此!你不希望使用者因為誤點而將已輸入的訊息傳送給錯誤的人。要修正這個問題,請加入一個key

這確保了當你選擇不同的收件人時,Chat元件將會從頭開始重新建立,包括其下方樹狀結構中的任何狀態。React 也會重新建立 DOM 元素,而不是重複使用它們。

現在切換收件人總是會清空文字欄位:

Deep Dive
保留已移除元件的狀態

總結

  • 只要相同的元件在相同的位置被渲染,React 就會保留其狀態。
  • 狀態並非儲存在 JSX 標籤中。它與你放置該 JSX 的樹狀結構位置相關聯。
  • 你可以透過賦予子樹不同的 key 來強制其重置狀態。
  • 不要巢狀定義元件,否則你可能會意外重置狀態。

Try out some challenges

Challenge 1 of 5:Fix disappearing input text #

This example shows a message when you press the button. However, pressing the button also accidentally resets the input. Why does this happen? Fix it so that pressing the button does not reset the input text.