參考文章
更新 state 中的數(shù)組
數(shù)組是另外一種可以存儲在 state 中的 JavaScript 對象,它雖然是可變的,但是卻應(yīng)該被視為不可變。同對象一樣,當(dāng)想要更新存儲于 state 中的數(shù)組時,需要創(chuàng)建一個新的數(shù)組(或者創(chuàng)建一份已有數(shù)組的拷貝值),并使用新數(shù)組設(shè)置 state。
在沒有 mutation 的前提下更新數(shù)組
在 JavaScript 中,數(shù)組只是另一種對象。同對象一樣,需要將 React state 中的數(shù)組視為只讀的。這意味著不應(yīng)該使用類似于 arr[0] = 'bird'
這樣的方式來重新分配數(shù)組中的元素,也不應(yīng)該使用會直接修改原始數(shù)組的方法,例如 push()
和 pop()
。
相反,每次要更新一個數(shù)組時,需要把一個新的數(shù)組傳入 state 的 setting 方法中。為此,可以通過使用像 filter()
和 map()
這樣不會直接修改原始值的方法,從原始數(shù)組生成一個新的數(shù)組。然后就可以將 state 設(shè)置為這個新生成的數(shù)組。
下面是常見數(shù)組操作的參考表。當(dāng)操作 React state 中的數(shù)組時,需要避免使用左列的方法,而首選右列的方法:
避免使用 (會改變原始數(shù)組) | 推薦使用 (會返回一個新數(shù)組) | |
---|---|---|
添加元素 |
push ,unshift
|
concat ,[...arr] 展開語法(例子) |
刪除元素 |
pop ,shift ,splice
|
filter ,slice (例子) |
替換元素 |
splice ,arr[i] = ... 賦值 |
map (例子) |
排序 |
reverse ,sort
|
先將數(shù)組復(fù)制一份(例子) |
或者,可以使用 Immer ,這樣便可以使用表格中的所有方法了。
注意:不幸的是,雖然 slice
和 splice
的名字相似,但作用卻迥然不同:
-
slice
可以拷貝數(shù)組或是數(shù)組的一部分。 -
splice
會直接修改 原始數(shù)組(插入或者刪除元素)。
在 React 中,更多情況下會使用 slice
,因為不想改變 state 中的對象或數(shù)組。
向數(shù)組中添加元素
應(yīng)該創(chuàng)建一個 新 數(shù)組,其包含了原始數(shù)組的所有元素 以及 一個在末尾的新元素。這可以通過很多種方法實現(xiàn),最簡單的一種就是使用 ...
數(shù)組展開 語法:
setArtists( // 替換 state
[ // 是通過傳入一個新數(shù)組實現(xiàn)的
...artists, // 新數(shù)組包含原數(shù)組的所有元素
{ id: nextId++, name: name } // 并在末尾添加了一個新的元素
]
);
示例代碼:
import { useState } from 'react';
let nextId = 0;
export default function List() {
const [name, setName] = useState('');
const [artists, setArtists] = useState([]);
return (
<>
<h1>振奮人心的雕塑家們:</h1>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
<button onClick={() => {
setArtists([
...artists,
{ id: nextId++, name: name }
]);
}}>添加</button>
<ul>
{artists.map(artist => (
<li key={artist.id}>{artist.name}</li>
))}
</ul>
</>
);
}
數(shù)組展開運算符還允許把新添加的元素放在原始的 ...artists
之前:
setArtists([
{ id: nextId++, name: name },
...artists // 將原數(shù)組中的元素放在末尾
]);
這樣一來,展開操作就可以完成 push()
和 unshift()
的工作,將新元素添加到數(shù)組的末尾和開頭。
從數(shù)組中刪除元素
從數(shù)組中刪除一個元素最簡單的方法就是將它過濾出去。換句話說,需要生成一個不包含該元素的新數(shù)組。這可以通過 filter
方法實現(xiàn),例如:
import { useState } from 'react';
let initialArtists = [
{ id: 0, name: 'Marta Colvin Andrade' },
{ id: 1, name: 'Lamidi Olonade Fakeye'},
{ id: 2, name: 'Louise Nevelson'},
];
export default function List() {
const [artists, setArtists] = useState(
initialArtists
);
return (
<>
<h1>振奮人心的雕塑家們:</h1>
<ul>
{artists.map(artist => (
<li key={artist.id}>
{artist.name}{' '}
<button onClick={() => {
setArtists(
artists.filter(a =>
a.id !== artist.id
)
);
}}>
刪除
</button>
</li>
))}
</ul>
</>
);
}
點擊“刪除”按鈕幾次,并且查看按鈕處理點擊事件的代碼。
setArtists(
artists.filter(a => a.id !== artist.id)
);
這里,artists.filter(s => s.id !== artist.id)
表示“創(chuàng)建一個新的數(shù)組,該數(shù)組由那些 ID 與 artists.id
不同的 artists
組成”。換句話說,每個 artist 的“刪除”按鈕會把 那一個 artist 從原始數(shù)組中過濾掉,并使用過濾后的數(shù)組再次進(jìn)行渲染。注意,filter
并不會改變原始數(shù)組。
轉(zhuǎn)換數(shù)組
如果想改變數(shù)組中的某些或全部元素,可以用 map()
創(chuàng)建一個新數(shù)組。傳入 map
的函數(shù)決定了要根據(jù)每個元素的值或索引(或二者都要)對元素做何處理。
在下面的例子中,一個數(shù)組記錄了兩個圓形和一個正方形的坐標(biāo)。當(dāng)點擊按鈕時,僅有兩個圓形會向下移動 100 像素。這是通過使用 map()
生成一個新數(shù)組實現(xiàn)的。
import { useState } from 'react';
let initialShapes = [
{ id: 0, type: 'circle', x: 50, y: 100 },
{ id: 1, type: 'square', x: 150, y: 100 },
{ id: 2, type: 'circle', x: 250, y: 100 },
];
export default function ShapeEditor() {
const [shapes, setShapes] = useState(
initialShapes
);
function handleClick() {
const nextShapes = shapes.map(shape => {
if (shape.type === 'square') {
// 不作改變
return shape;
} else {
// 返回一個新的圓形,位置在下方 50px 處
return {
...shape,
y: shape.y + 50,
};
}
});
// 使用新的數(shù)組進(jìn)行重渲染
setShapes(nextShapes);
}
return (
<>
<button onClick={handleClick}>
所有圓形向下移動!
</button>
{shapes.map(shape => (
<div
key={shape.id}
style={{
background: 'purple',
position: 'absolute',
left: shape.x,
top: shape.y,
borderRadius:
shape.type === 'circle'
? '50%' : '',
width: 20,
height: 20,
}} />
))}
</>
);
}
替換數(shù)組中的元素
想要替換數(shù)組中一個或多個元素是非常常見的。類似 arr[0] = 'bird'
這樣的賦值語句會直接修改原始數(shù)組,所以在這種情況下,也應(yīng)該使用 map
。
要替換一個元素,請使用 map
創(chuàng)建一個新數(shù)組。在 map
回調(diào)里,第二個參數(shù)是元素的索引。使用索引來判斷最終是返回原始的元素(即回調(diào)的第一個參數(shù))還是替換成其他值:
import { useState } from 'react';
let initialCounters = [
0, 0, 0
];
export default function CounterList() {
const [counters, setCounters] = useState(
initialCounters
);
function handleIncrementClick(index) {
const nextCounters = counters.map((c, i) => {
if (i === index) {
// 遞增被點擊的計數(shù)器數(shù)值
return c + 1;
} else {
// 其余部分不發(fā)生變化
return c;
}
});
setCounters(nextCounters);
}
return (
<ul>
{counters.map((counter, i) => (
<li key={i}>
{counter}
<button onClick={() => {
handleIncrementClick(i);
}}>+1</button>
</li>
))}
</ul>
);
}
向數(shù)組中插入元素
有時,也許想向數(shù)組特定位置插入一個元素,這個位置既不在數(shù)組開頭,也不在末尾。為此,可以將數(shù)組展開運算符 ...
和 slice()
方法一起使用。slice()
方法讓從數(shù)組中切出“一片”。為了將元素插入數(shù)組,需要先展開原數(shù)組在插入點之前的切片,然后插入新元素,最后展開原數(shù)組中剩下的部分。
下面的例子中,插入按鈕總是會將元素插入到數(shù)組中索引為 1
的位置。
import { useState } from 'react';
let nextId = 3;
const initialArtists = [
{ id: 0, name: 'Marta Colvin Andrade' },
{ id: 1, name: 'Lamidi Olonade Fakeye'},
{ id: 2, name: 'Louise Nevelson'},
];
export default function List() {
const [name, setName] = useState('');
const [artists, setArtists] = useState(
initialArtists
);
function handleClick() {
const insertAt = 1; // 可能是任何索引
const nextArtists = [
// 插入點之前的元素:
...artists.slice(0, insertAt),
// 新的元素:
{ id: nextId++, name: name },
// 插入點之后的元素:
...artists.slice(insertAt)
];
setArtists(nextArtists);
setName('');
}
return (
<>
<h1>振奮人心的雕塑家們:</h1>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
<button onClick={handleClick}>
插入
</button>
<ul>
{artists.map(artist => (
<li key={artist.id}>{artist.name}</li>
))}
</ul>
</>
);
}
其他改變數(shù)組的情況
總會有一些事,是僅僅依靠展開運算符和 map()
或者 filter()
等不會直接修改原值的方法所無法做到的。例如,可能想翻轉(zhuǎn)數(shù)組,或是對數(shù)組排序。而 JavaScript 中的 reverse()
和 sort()
方法會改變原數(shù)組,所以無法直接使用它們。
然而,可以先拷貝這個數(shù)組,再改變這個拷貝后的值。
例如:
import { useState } from 'react';
const initialList = [
{ id: 0, title: 'Big Bellies' },
{ id: 1, title: 'Lunar Landscape' },
{ id: 2, title: 'Terracotta Army' },
];
export default function List() {
const [list, setList] = useState(initialList);
function handleClick() {
const nextList = [...list];
nextList.reverse();
setList(nextList);
}
return (
<>
<button onClick={handleClick}>
翻轉(zhuǎn)
</button>
<ul>
{list.map(artwork => (
<li key={artwork.id}>{artwork.title}</li>
))}
</ul>
</>
);
}
在這段代碼中,先使用 [...list]
展開運算符創(chuàng)建了一份數(shù)組的拷貝值。當(dāng)有了這個拷貝值后,就可以使用像 nextList.reverse()
或 nextList.sort()
這樣直接修改原數(shù)組的方法。甚至可以通過 nextList[0] = "something"
這樣的方式對數(shù)組中的特定元素進(jìn)行賦值。
然而,即使拷貝了數(shù)組,還是不能直接修改其內(nèi)部的元素。這是因為數(shù)組的拷貝是淺拷貝——新的數(shù)組中依然保留了與原始數(shù)組相同的元素。因此,如果修改了拷貝數(shù)組內(nèi)部的某個對象,其實正在直接修改當(dāng)前的 state。舉個例子,像下面的代碼就會帶來問題。
const nextList = [...list];
nextList[0].seen = true; // 問題:直接修改了 list[0] 的值
setList(nextList);
雖然 nextList
和 list
是兩個不同的數(shù)組,nextList[0]
和 list[0]
卻指向了同一個對象。因此,通過改變 nextList[0].seen
,list[0].seen
的值也被改變了。這是一種 state 的 mutation 操作,應(yīng)該避免這么做!可以用類似于 更新嵌套的 JavaScript 對象 的方式解決這個問題——拷貝想要修改的特定元素,而不是直接修改它。下面是具體的操作。
更新數(shù)組內(nèi)部的對象
對象并不是 真的 位于數(shù)組“內(nèi)部”。可能他們在代碼中看起來像是在數(shù)組“內(nèi)部”,但其實數(shù)組中的每個對象都是這個數(shù)組“指向”的一個存儲于其它位置的值。這就是當(dāng)在處理類似 list[0]
這樣的嵌套字段時需要格外小心的原因。其他人的藝術(shù)品清單可能指向了數(shù)組的同一個元素!
當(dāng)更新一個嵌套的 state 時,需要從想要更新的地方創(chuàng)建拷貝值,一直這樣,直到頂層。 讓我們看一下這該怎么做。
在下面的例子中,兩個不同的藝術(shù)品清單有著相同的初始 state。他們本應(yīng)該互不影響,但是因為一次 mutation,他們的 state 被意外地共享了,勾選一個清單中的事項會影響另外一個清單:
import { useState } from 'react';
let nextId = 3;
const initialList = [
{ id: 0, title: 'Big Bellies', seen: false },
{ id: 1, title: 'Lunar Landscape', seen: false },
{ id: 2, title: 'Terracotta Army', seen: true },
];
export default function BucketList() {
const [myList, setMyList] = useState(initialList);
const [yourList, setYourList] = useState(
initialList
);
function handleToggleMyList(artworkId, nextSeen) {
const myNextList = [...myList];
const artwork = myNextList.find(
a => a.id === artworkId
);
artwork.seen = nextSeen;
setMyList(myNextList);
}
function handleToggleYourList(artworkId, nextSeen) {
const yourNextList = [...yourList];
const artwork = yourNextList.find(
a => a.id === artworkId
);
artwork.seen = nextSeen;
setYourList(yourNextList);
}
return (
<>
<h1>藝術(shù)愿望清單</h1>
<h2>我想看的藝術(shù)清單:</h2>
<ItemList
artworks={myList}
onToggle={handleToggleMyList} />
<h2>你想看的藝術(shù)清單:</h2>
<ItemList
artworks={yourList}
onToggle={handleToggleYourList} />
</>
);
}
function ItemList({ artworks, onToggle }) {
return (
<ul>
{artworks.map(artwork => (
<li key={artwork.id}>
<label>
<input
type="checkbox"
checked={artwork.seen}
onChange={e => {
onToggle(
artwork.id,
e.target.checked
);
}}
/>
{artwork.title}
</label>
</li>
))}
</ul>
);
}
問題出在下面這段代碼中:
const myNextList = [...myList];
const artwork = myNextList.find(a => a.id === artworkId);
artwork.seen = nextSeen; // 問題:直接修改了已有的元素
setMyList(myNextList);
雖然 myNextList
這個數(shù)組是新的,但是其內(nèi)部的元素本身與原數(shù)組 myList
是相同的。因此,修改 artwork.seen
,其實是在修改原始的 artwork 對象。而這個 artwork 對象也被 yourList
使用,這樣就帶來了 bug。這樣的 bug 可能難以想到,但好在如果避免直接修改 state,它們就會消失。
可以使用 map
在沒有 mutation 的前提下將一個舊的元素替換成更新的版本。
setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// 創(chuàng)建包含變更的新對象
return { ...artwork, seen: nextSeen };
} else {
// 沒有變更
return artwork;
}
}));
此處的 ...
是一個對象展開語法,被用來創(chuàng)建一個對象的拷貝.
通過這種方式,沒有任何現(xiàn)有的 state 中的元素會被改變,bug 也就被修復(fù)了。
通常來講,應(yīng)該只直接修改剛剛創(chuàng)建的對象。如果正在插入一個新的 artwork,可以修改它,但是如果想要改變的是 state 中已經(jīng)存在的東西,就需要先拷貝一份了。
使用 Immer 編寫簡潔的更新邏輯
在沒有 mutation 的前提下更新嵌套數(shù)組可能會變得有點重復(fù)。就像對對象一樣:
- 通常情況下,應(yīng)該不需要更新處于非常深層級的 state 。如果有此類需求,或許需要調(diào)整一下數(shù)據(jù)的結(jié)構(gòu),讓數(shù)據(jù)變得扁平一些。
- 如果想改變 state 的數(shù)據(jù)結(jié)構(gòu),可以使用 Immer ,它讓你可以繼續(xù)使用方便的,但會直接修改原值的語法,并負(fù)責(zé)為你生成拷貝值。
下面是我們用 Immer 來重寫的藝術(shù)愿望清單的例子:
import { useState } from 'react';
import { useImmer } from 'use-immer';
let nextId = 3;
const initialList = [
{ id: 0, title: 'Big Bellies', seen: false },
{ id: 1, title: 'Lunar Landscape', seen: false },
{ id: 2, title: 'Terracotta Army', seen: true },
];
export default function BucketList() {
const [myList, updateMyList] = useImmer(
initialList
);
const [yourList, updateYourList] = useImmer(
initialList
);
function handleToggleMyList(id, nextSeen) {
updateMyList(draft => {
const artwork = draft.find(a =>
a.id === id
);
artwork.seen = nextSeen;
});
}
function handleToggleYourList(artworkId, nextSeen) {
updateYourList(draft => {
const artwork = draft.find(a =>
a.id === artworkId
);
artwork.seen = nextSeen;
});
}
return (
<>
<h1>藝術(shù)愿望清單</h1>
<h2>我想看的藝術(shù)清單:</h2>
<ItemList
artworks={myList}
onToggle={handleToggleMyList} />
<h2>你想看的藝術(shù)清單:</h2>
<ItemList
artworks={yourList}
onToggle={handleToggleYourList} />
</>
);
}
function ItemList({ artworks, onToggle }) {
return (
<ul>
{artworks.map(artwork => (
<li key={artwork.id}>
<label>
<input
type="checkbox"
checked={artwork.seen}
onChange={e => {
onToggle(
artwork.id,
e.target.checked
);
}}
/>
{artwork.title}
</label>
</li>
))}
</ul>
);
}
請注意當(dāng)使用 Immer 時,類似 artwork.seen = nextSeen
這種會產(chǎn)生 mutation 的語法不會再有任何問題了:
updateMyTodos(draft => {
const artwork = draft.find(a => a.id === artworkId);
artwork.seen = nextSeen;
});
這是因為并不是在直接修改原始的 state,而是在修改 Immer 提供的一個特殊的 draft
對象。同理,也可以為 draft
的內(nèi)容使用 push()
和 pop()
這些會直接修改原值的方法。文章來源:http://www.zghlxwxcb.cn/news/detail-655261.html
在幕后,Immer 總是會根據(jù)對 draft
的修改來從頭開始構(gòu)建下一個 state。這使得你的事件處理程序非常的簡潔,同時也不會直接修改 state。文章來源地址http://www.zghlxwxcb.cn/news/detail-655261.html
摘要
- 可以把數(shù)組放入 state 中,但不應(yīng)該直接修改它。
- 不要直接修改數(shù)組,而是創(chuàng)建它的一份 新的 拷貝,然后使用新的數(shù)組來更新它的狀態(tài)。
- 可以使用
[...arr, newItem]
這樣的數(shù)組展開語法來向數(shù)組中添加元素。 - 可以使用
filter()
和map()
來創(chuàng)建一個經(jīng)過過濾或者變換的數(shù)組。 - 可以使用 Immer 來保持代碼簡潔。
到了這里,關(guān)于React 18 更新 state 中的數(shù)組的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!