更新狀態中的陣列
在 JavaScript 中,陣列是可變的,但當你將它們儲存在狀態中時,應該將它們視為不可變的。就像物件一樣,當你想要更新儲存在狀態中的陣列時,你需要建立一個新的陣列(或複製現有的陣列),然後將狀態設定為使用這個新陣列。
您將學習
- 如何在 React 狀態中新增、移除或變更陣列中的項目
- 如何更新陣列內的物件
- 如何使用 Immer 減少陣列複製的重複性
無突變地更新陣列
在 JavaScript 中,陣列只是另一種物件。如同物件,你應該將 React 狀態中的陣列視為唯讀。這意味著你不應該像arr[0] = 'bird'這樣重新指派陣列內的項目,也不應該使用會突變陣列的方法,例如push()和pop()。
相反地,每次你想要更新陣列時,你應該傳遞一個新的陣列給你的狀態設定函式。要做到這一點,你可以透過呼叫其非突變方法(例如filter()和map())從狀態中的原始陣列建立一個新陣列。然後你可以將你的狀態設定為產生的新陣列。
以下是一個常見陣列操作的參考表格。當處理 React 狀態內的陣列時,你需要避免左欄的方法,而應優先使用右欄的方法:
或者,你可以使用 Immer,它允許你使用兩欄中的方法。
新增至陣列
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"來賦值給單個項目。
然而,即使您複製了一個陣列,也不能直接變異其中現有的項目。內部的項目。這是因為複製是淺層的——新陣列將包含與原始陣列相同的項目。因此,如果您修改複製陣列中的物件,就是在變異現有的狀態。例如,這樣的程式碼是有問題的。
雖然nextList和list是兩個不同的陣列,但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:
