v19.2Latest

選擇狀態結構

良好的狀態結構設計,可以區分出一個易於修改和除錯的元件,與一個不斷產生錯誤的元件。以下是設計狀態結構時應考慮的一些技巧。

您將學習
  • 何時使用單一狀態變數與多個狀態變數
  • 組織狀態時應避免的事項
  • 如何修正狀態結構的常見問題

狀態結構設計原則

當您編寫一個包含某些狀態的元件時,您必須決定要使用多少個狀態變數以及它們的資料結構應該是什麼樣子。雖然即使使用次優的狀態結構也有可能編寫出正確的程式,但有一些原則可以指導您做出更好的選擇:

  1. 將相關狀態分組。如果您總是同時更新兩個或多個狀態變數,請考慮將它們合併為單一狀態變數。
  2. 避免狀態矛盾。當狀態的結構方式可能導致多個狀態片段相互矛盾或「不一致」時,您就為錯誤留下了空間。請盡量避免這種情況。
  3. 避免冗餘狀態。如果您可以在渲染期間從元件的 props 或其現有狀態變數計算出某些資訊,則不應將該資訊放入該元件的狀態中。
  4. 避免狀態重複。當相同的資料在多個狀態變數之間重複,或在巢狀物件內重複時,很難保持它們同步。請盡可能減少重複。
  5. 避免深度巢狀狀態。深度層次化的狀態更新起來不太方便。在可能的情況下,請優先採用扁平化的方式來結構化狀態。

這些原則背後的目標是使狀態易於更新而不引入錯誤。從狀態中移除冗餘和重複的資料有助於確保其所有部分保持同步。這類似於資料庫工程師可能希望「正規化」資料庫結構以減少錯誤的機會。借用愛因斯坦的話來說,「讓你的狀態盡可能簡單——但不要過於簡單。」

現在讓我們看看這些原則在實際中如何應用。

有時您可能不確定應該使用單一狀態變數還是多個狀態變數。

您應該這樣做嗎?

還是這樣做?

從技術上講,這兩種方法您都可以使用。但是如果某些兩個狀態變數總是一起變化,將它們統一為單一狀態變數可能是個好主意。這樣您就不會忘記始終保持它們同步,就像在這個例子中,移動游標會同時更新紅點的兩個座標:

另一種將資料分組到物件或陣列的情況是當您不知道需要多少個狀態片段時。例如,當您有一個允許使用者新增自訂欄位的表單時,這種做法很有幫助。

陷阱

如果您的狀態變數是一個物件,請記住您無法僅更新其中的一個欄位而不明確複製其他欄位。例如,在上面的例子中,您不能執行setPosition({ x: 100 }),因為這樣會完全沒有y屬性!相反,如果您想單獨設定x,您應該執行setPosition({ ...position, x: 100 }),或者將它們拆分為兩個狀態變數並執行setX(100)

避免狀態矛盾

這是一個包含isSendingisSent狀態變數的飯店意見回饋表單:

雖然這段程式碼可以運作,但它為「不可能」的狀態留下了空間。例如,如果你忘記同時呼叫setIsSentsetIsSending,你可能會陷入 isSendingisSent同時為true 的情況。你的元件越複雜,就越難理解發生了什麼事。

由於isSendingisSent絕不應該同時為true,更好的做法是將它們替換為一個 status 狀態變數,它可以取 三種有效狀態之一:'typing'(初始)、'sending''sent'

你仍然可以宣告一些常數以提高可讀性:

但它們不是狀態變數,因此你無需擔心它們彼此之間會失去同步。

避免冗餘狀態

如果你可以在渲染期間從元件的 props 或其現有的狀態變數計算出某些資訊,你不應該將該資訊放入該元件的狀態中。

例如,看看這個表單。它可以運作,但你能找出其中任何冗餘的狀態嗎?

這個表單有三個狀態變數:firstNamelastNamefullName。然而,fullName是多餘的。您總是可以從fullName在渲染期間計算出firstNamelastName,因此可以將其從狀態中移除。

您可以這樣做:

在這裡,fullName不是一個狀態變數。相反,它是在渲染期間計算出來的:

因此,事件處理函式不需要做任何特殊操作來更新它。當您呼叫setFirstNamesetLastName時,會觸發重新渲染,然後下一個fullName就會根據最新的資料計算出來。

Deep Dive
不要在狀態中鏡像屬性

避免狀態中的重複

這個選單列表元件讓您可以從幾種旅行零食中選擇一種:

目前,它將選中的項目作為一個物件儲存在selectedItem狀態變數中。然而,這並不好: selectedItem 的內容與 items列表中的某個項目是同一個物件。 這意味著關於該項目的資訊在兩個地方重複了。

為什麼這是個問題?讓我們讓每個項目都可編輯:

請注意,如果您先點擊某個項目的「Choose」,然後再編輯它,輸入框會更新,但底部的標籤卻沒有反映編輯內容。這是因為您重複了狀態,並且忘記更新 selectedItem

雖然您也可以更新selectedItem,但更簡單的解決方法是消除重複。在這個例子中,與其使用一個selectedItem 物件(這會與 items內的物件產生重複),不如在狀態中儲存selectedId然後透過在selectedItem陣列中搜尋具有該 ID 的項目來取得items

之前的狀態是這樣重複的:

  • items = [{ id: 0, title: 'pretzels'}, ...]
  • selectedItem = {id: 0, title: 'pretzels'}

但修改後變成了這樣:

  • items = [{ id: 0, title: 'pretzels'}, ...]
  • selectedId = 0

重複消失了,你只保留了必要的狀態!

現在如果你編輯已選擇的項目,下方的訊息會立即更新。這是因為setItems會觸發重新渲染,而items.find(...)會找到具有更新後標題的項目。你不需要將已選擇的項目保存在狀態中,因為只有已選擇的 ID是必要的。其餘的可以在渲染期間計算出來。

避免深度巢狀的狀態

想像一個由行星、大陸和國家組成的旅行計畫。你可能會想使用巢狀物件和陣列來建構其狀態,就像這個例子:

現在假設你想添加一個按鈕來刪除你已經去過的地方。你會怎麼做?更新巢狀狀態涉及從發生變化的部分開始,一路向上複製物件。刪除一個深度巢狀的地方將涉及複製其整個父級地方鏈。這樣的程式碼可能會非常冗長。

如果狀態過於巢狀以至於難以更新,請考慮將其「扁平化」。這裡有一種你可以重構這些資料的方法。與其使用樹狀結構,讓每個place都有一個包含其子地方的陣列,不如讓每個地方持有一個包含其子地方 ID的陣列。然後儲存從每個地方 ID 到對應地方的映射。

這種資料重構可能會讓你想起看到資料庫表格:

現在狀態已經是「扁平化」(也稱為「正規化」)了,更新巢狀項目變得更加容易。

現在要移除一個地點,你只需要更新兩個層級的狀態:

  • 父級地點的更新版本應從其childIds陣列中排除已移除的 ID。
  • 根「表格」物件的更新版本應包含父級地點的更新版本。

以下是一個你可以如何操作的範例:

您可以隨意地嵌套狀態,但將其「扁平化」可以解決許多問題。這使得狀態更容易更新,並有助於確保您不會在巢狀物件的不同部分出現重複。

有時,您也可以透過將部分巢狀狀態移至子元件中來減少狀態嵌套。這對於不需要儲存的暫時性 UI 狀態(例如某個項目是否處於懸停狀態)非常有效。

總結

  • 如果兩個狀態變數總是同時更新,請考慮將它們合併為一個。
  • 謹慎選擇您的狀態變數,以避免建立「不可能」的狀態。
  • 以一種能減少更新時出錯機率的方式來組織您的狀態。
  • 避免冗餘和重複的狀態,這樣您就不需要保持它們同步。
  • 不要將 props放入 狀態中,除非您特意想要阻止更新。
  • 對於像選擇這樣的 UI 模式,請將 ID 或索引保留在狀態中,而不是物件本身。
  • 如果更新深度巢狀的狀態很複雜,請嘗試將其扁平化。

Try out some challenges

Challenge 1 of 4:Fix a component that’s not updating #

This Clock component receives two props: color and time. When you select a different color in the select box, the Clock component receives a different color prop from its parent component. However, for some reason, the displayed color doesn’t update. Why? Fix the problem.