v19.2Latest

更新狀態中的陣列

在 JavaScript 中,陣列是可變的,但當你將它們儲存在狀態中時,應該將它們視為不可變的。就像物件一樣,當你想要更新儲存在狀態中的陣列時,你需要建立一個新的陣列(或複製現有的陣列),然後將狀態設定為使用這個新陣列。

您將學習
  • 如何在 React 狀態中新增、移除或變更陣列中的項目
  • 如何更新陣列內的物件
  • 如何使用 Immer 減少陣列複製的重複性

無突變地更新陣列

在 JavaScript 中,陣列只是另一種物件。如同物件你應該將 React 狀態中的陣列視為唯讀。這意味著你不應該像arr[0] = 'bird'這樣重新指派陣列內的項目,也不應該使用會突變陣列的方法,例如push()pop()

相反地,每次你想要更新陣列時,你應該傳遞一個新的陣列給你的狀態設定函式。要做到這一點,你可以透過呼叫其非突變方法(例如filter()map())從狀態中的原始陣列建立一個新陣列。然後你可以將你的狀態設定為產生的新陣列。

以下是一個常見陣列操作的參考表格。當處理 React 狀態內的陣列時,你需要避免左欄的方法,而應優先使用右欄的方法:

避免(會突變陣列)優先使用(回傳新陣列)
新增pushunshiftconcat[...arr]展開語法(範例
移除popshiftsplicefilterslice範例
替換splicearr[i] = ...賦值map範例
排序reversesort先複製陣列(範例

或者,你可以使用 Immer,它允許你使用兩欄中的方法。

陷阱

不幸的是,slicesplice名稱相似但卻截然不同:

  • slice允許你複製陣列或其中的一部分。
  • splice會突變陣列(以插入或刪除項目)。

在 React 中,你會更頻繁地使用slice(沒有p!),因為你不希望突變狀態中的物件或陣列。更新物件解釋了什麼是突變以及為什麼不建議用於狀態。

新增至陣列

push()會突變陣列,這是你不想發生的:

相反地,建立一個新的陣列,其中包含現有項目以及末尾的一個新項目。有多種方法可以做到這一點,但最簡單的方法是使用...陣列展開語法:

現在它可以正確運作了:

陣列展開語法也可以讓您將項目放在原始前面...artists來將其前置:

這樣一來,展開語法可以同時完成push()(將項目添加到陣列末尾)和unshift()(將項目添加到陣列開頭)的工作。請在上面的沙盒中試試看!

從陣列中移除項目

從陣列中移除項目最簡單的方法是將其過濾掉。換句話說,您將產生一個不包含該項目的新陣列。為此,請使用filter方法,例如:

點擊幾次「Delete」按鈕,並查看其點擊處理函式。

在這裡,artists.filter(a => a.id !== artist.id)表示「建立一個由那些 ID 與artists不同的artist.id組成的陣列」。換句話說,每個藝術家的「Delete」按鈕都會將藝術家從陣列中過濾掉,然後請求使用結果陣列重新渲染。請注意,filter不會修改原始陣列。

轉換陣列

如果您想更改陣列中的部分或全部項目,可以使用map()來建立一個新的陣列。您傳遞給map的函式可以根據每個項目的資料或其索引(或兩者)來決定對每個項目做什麼。

在這個例子中,一個陣列儲存了兩個圓圈和一個正方形的座標。當您按下按鈕時,它只會將圓圈向下移動 50 像素。這是透過使用map()產生一個新的資料陣列來實現的:

替換陣列中的項目

特別常見的是想要替換陣列中的一個或多個項目。像arr[0] = 'bird'這樣的賦值會突變原始陣列,因此您也會想要為此使用map

要替換一個項目,請使用map建立一個新陣列。在您的map呼叫內部,您將收到項目索引作為第二個參數。使用它來決定是返回原始項目(第一個參數)還是其他內容:

插入到陣列中

有時,您可能想在一個既非開頭也非結尾的特定位置插入一個項目。為此,您可以使用...陣列展開語法配合slice()方法。slice()方法讓您可以切出陣列的一個「切片」。要插入一個項目,您將建立一個陣列,該陣列展開插入點之前的切片,然後是新項目,接著是原始陣列的其餘部分。

在這個例子中,插入按鈕總是在索引1處插入:

對陣列進行其他更改

有些操作僅靠展開語法和非變異方法(如map()filter())是無法完成的。例如,您可能想反轉或排序一個陣列。JavaScript的reverse()sort()方法會變異原始陣列,因此您不能直接使用它們。

但是,您可以先複製陣列,然後再對其進行更改。

例如:

在這裡,您使用[...list]展開語法首先建立原始陣列的副本。現在您有了一個副本,就可以使用變異方法,例如nextList.reverse()nextList.sort(),甚至可以使用nextList[0] = "something"來賦值給單個項目。

然而,即使您複製了一個陣列,也不能直接變異其中現有的項目。內部的項目。這是因為複製是淺層的——新陣列將包含與原始陣列相同的項目。因此,如果您修改複製陣列中的物件,就是在變異現有的狀態。例如,這樣的程式碼是有問題的。

雖然nextListlist是兩個不同的陣列,但nextList[0]list[0]指向同一個物件。因此,透過更改nextList[0].seen,您同時也在更改list[0].seen。這是一種狀態變異,您應該避免!您可以透過類似於更新巢狀 JavaScript 物件的方式來解決這個問題——複製您想要更改的單個項目,而不是變異它們。以下是具體做法。

更新陣列內的物件

物件並非真正「位於」陣列內部。它們在程式碼中可能看起來是「在裡面」,但陣列中的每個物件都是一個獨立的值,陣列「指向」它。這就是為什麼在更改像list[0]這樣的巢狀欄位時需要小心。另一個人的藝術品清單可能指向陣列的同一元素!

更新巢狀狀態時,您需要從想要更新的點開始建立副本,並一直向上直到頂層。讓我們看看這是如何運作的。

在這個例子中,兩個獨立的藝術品清單具有相同的初始狀態。它們本應是隔離的,但由於變異,它們的狀態意外地共享了,在一個清單中勾選方框會影響另一個清單:

問題出在這樣的程式碼中:

雖然 myNextList陣列本身是新的,但其中的項目本身 與原始 myList陣列中的項目是相同的。因此,更改artwork.seen 會改變 原始的藝術品項目。該藝術品項目也存在於yourList中,這就導致了錯誤。像這樣的錯誤可能難以思考,但幸運的是,如果你避免直接變異狀態,它們就會消失。

你可以使用map 來替換舊項目為其更新版本,而無需變異。

這裡的... 是物件展開語法,用於 建立物件的副本。

使用這種方法,沒有任何現有的狀態項目被變異,錯誤也就被修正了:

一般來說,你應該只變異你剛剛建立的物件。如果你要插入一個新的藝術品,你可以變異它,但如果你處理的是已經存在於狀態中的東西,你就需要建立一個副本。

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

在不變異的情況下更新巢狀陣列可能會變得有些重複。就像處理物件時一樣

  • 通常,你不需要更新超過幾層深的狀態。如果你的狀態物件非常深,你可能需要 以不同的方式重組它們,使其變得扁平。
  • 如果你不想改變你的狀態結構,你可能更喜歡使用 Immer,它允許你使用方便但會變異的語法進行編寫,並負責為你產生副本。

以下是使用 Immer 重寫的藝術品願望清單範例:

請注意,使用 Immer 時,artwork.seen = nextSeen這樣的突變現在是可以接受的:

這是因為您並非在突變原始的狀態,而是在突變 Immer 提供的一個特殊draft 物件。同樣地,您可以對 draft的內容套用突變方法,例如push()pop()

在幕後,Immer 總是根據您對 draft所做的更改,從頭開始建構下一個狀態。這使得您的事件處理函式非常簡潔,且永遠不會突變狀態。

總結

  • 您可以將陣列放入狀態中,但不能更改它們。
  • 與其突變一個陣列,不如建立它的版本,並將狀態更新為該版本。
  • 您可以使用[...arr, newItem]陣列展開語法來建立包含新項目的陣列。
  • 您可以使用filter()map()來建立包含已篩選或已轉換項目的新陣列。
  • 您可以使用 Immer 來保持程式碼簡潔。

Try out some challenges

Challenge 1 of 4:Update an item in the shopping cart #

Fill in the handleIncreaseClick logic so that pressing ”+” increases the corresponding number: