v19.2Latest

更新狀態中的物件

狀態可以儲存任何類型的 JavaScript 值,包括物件。但你不應該直接更改儲存在 React 狀態中的物件。相反地,當你想要更新物件時,你需要建立一個新的物件(或複製現有的物件),然後將狀態設定為使用該副本。

您將學習
  • 如何正確更新 React 狀態中的物件
  • 如何在不突變的情況下更新巢狀物件
  • 什麼是不可變性,以及如何不破壞它
  • 如何使用 Immer 減少物件複製的重複性

什麼是突變?

你可以在狀態中儲存任何類型的 JavaScript 值。

到目前為止,你一直在處理數字、字串和布林值。這些類型的 JavaScript 值是「不可變的」,意思是不可更改或「唯讀」。你可以觸發重新渲染來替換一個值:

狀態x0變更為5,但數字0本身並未改變。在 JavaScript 中,無法對內建的基本型別值(如數字、字串和布林值)進行任何更改。

現在考慮狀態中的一個物件:

從技術上講,可以更改物件本身的內容。這稱為突變:

然而,儘管 React 狀態中的物件在技術上是可變的,你應該將它們視為如同它們是不可變的——就像數字、布林值和字串一樣。不應該突變它們,而應該總是替換它們。

將狀態視為唯讀

換句話說,你應該將放入狀態中的任何 JavaScript 物件視為唯讀。

這個範例在狀態中儲存一個物件來表示目前的指標位置。當你觸碰或移動游標到預覽區域時,紅點應該會移動。但紅點停留在初始位置:

問題出在這段程式碼。

這段程式碼修改了分配給position的物件,該物件來自先前的渲染。但由於沒有使用狀態設定函式,React 不知道物件已經改變。因此 React 沒有做出任何回應。這就像在吃完飯後才試圖更改訂單。雖然在某些情況下突變狀態可能有效,但我們不建議這樣做。你應該將在渲染中存取的狀態值視為唯讀。

要在這種情況下實際觸發重新渲染建立一個新的物件並將其傳遞給狀態設定函式:

透過setPosition,你告訴 React:

  • 用這個新物件替換position
  • 並再次渲染此元件

請注意,現在當你觸碰或將游標懸停在預覽區域時,紅點會跟隨你的指標:

Deep Dive
局部變異是沒問題的

使用展開語法複製物件

在前一個例子中,position物件總是根據當前游標位置全新建立的。但通常,你會希望將既存資料作為你正在建立的新物件的一部分包含進去。例如,你可能只想更新表單中的一個欄位,但保留所有其他欄位的先前值。

這些輸入欄位無法運作,因為onChange處理函式變異了狀態:

例如,這行程式碼變異了來自先前渲染的狀態:

要獲得你期望行為的可靠方法是建立一個新物件並將其傳遞給setPerson。但在這裡,你還希望將既存資料複製到其中,因為只有其中一個欄位發生了變化:

你可以使用...物件展開語法,這樣你就不需要分別複製每個屬性。

現在表單可以運作了!

請注意,你並沒有為每個輸入欄位宣告一個獨立的狀態變數。對於大型表單,將所有資料分組在一個物件中非常方便——只要你正確地更新它!

請注意,...展開語法是「淺層」的——它只複製一層深度。這使得它很快,但這也意味著如果你想更新一個巢狀屬性,你將需要使用它不止一次。

更新巢狀物件

考慮像這樣的巢狀物件結構:

如果你想更新 person.artwork.city,使用突變的方式很清楚該怎麼做:

但在 React 中,你將狀態視為不可變的!為了改變city,你需要先產生新的 artwork物件(預先填入前一個物件的資料),然後再產生指向新artwork 的新 person物件:

或者,寫成單一函式呼叫:

這有點囉嗦,但在許多情況下運作良好:

Deep Dive
物件並非真正的巢狀結構

使用 Immer 編寫簡潔的更新邏輯

如果你的狀態是深度巢狀的,你可能會想考慮將其扁平化。但是,如果你不想改變狀態結構,你可能會偏好使用巢狀展開的捷徑。Immer是一個流行的函式庫,它讓你使用方便但會突變的語法來編寫,並負責為你產生副本。使用 Immer 時,你寫的程式碼看起來像是在「違反規則」並突變一個物件:

但與常規的突變不同,它不會覆蓋過去的狀態!

Deep Dive
Immer 是如何運作的?

要嘗試 Immer:

  1. 執行npm install use-immer以將 Immer 添加為依賴項
  2. 然後將 import { useState } from 'react'替換為import { useImmer } from 'use-immer'

以下是轉換為使用 Immer 的上述範例:

請注意事件處理函式變得簡潔了多少。你可以在單個元件中隨意混合使用useStateuseImmer。Immer 是保持更新處理函式簡潔的好方法,特別是當你的狀態中存在巢狀結構,且複製物件會導致重複程式碼時。

Deep Dive
為什麼不建議在 React 中突變狀態?

總結

  • 將 React 中的所有狀態視為不可變的。
  • 當你將物件儲存在狀態中時,對其進行變異不會觸發重新渲染,並且會改變先前渲染「快照」中的狀態。
  • 與其變異物件,不如建立它的版本,並透過將狀態設定為該版本來觸發重新渲染。
  • 你可以使用{...obj, something: 'newValue'}物件展開語法來建立物件的副本。
  • 展開語法是淺層的:它只會複製一層深度。
  • 要更新巢狀物件,你需要從你正在更新的位置開始,一路向上建立副本。
  • 為了減少重複的複製程式碼,請使用 Immer。

Try out some challenges

Challenge 1 of 3:Fix incorrect state updates #

This form has a few bugs. Click the button that increases the score a few times. Notice that it does not increase. Then edit the first name, and notice that the score has suddenly “caught up” with your changes. Finally, edit the last name, and notice that the score has disappeared completely.

Your task is to fix all of these bugs. As you fix them, explain why each of them happens.