在JavaScript中Array是可变数据,但是当将它们存储在state中时,应该将它们视为不可变数据。和对象一样,当你更新存储在state的array时,需要新建一个新的数组(或是拷贝一个现有数组),然后给state设置为新的array。
不能直接修改array
在Javascript中,array属于object。就像object一样,把state中数组看作仅仅可读的,不能直接修改数组本身。不能给数组的元素重新赋值(如arr[0] = 'bird'),不能使用一些方法操作数组,如push()和pop。
如果你想修改数组,给设置state函数传一个新的数组。可以通过调用不改变数组本身的方法如filter()和map()创建一个新的数组。
下面是一个常见数组操作的参考表。当在React state中处理数组时,避免使用左列中的方法,而使用右列中的方法:
| 不使用(改变数组)) | 使用(返回一个新数组) | |
|---|---|---|
| 增加 | push, unshift | concat, [...arr]展开语法 |
| 删除 | pop, shift, splice | filter, slice |
| 替换 | splice, arr[i] = ...重新赋值 | map |
| 排序 | reverse, sort | 先复制数组,然后对复制的新数组排序 |
向数组添加项
import { useState } from 'react';
let nextId = 0;
export default function List() {
const [name, setName] = useState('');
const [artists, setArtists] = useState([]);
return (
<>
<h1>Inspiring sculptors:</h1>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
<button onClick={() => {
artists.push({
id: nextId++,
name: name,
});
}}>Add</button>
<ul>
{artists.map(artist => (
<li key={artist.id}>{artist.name}</li>
))}
</ul>
</>
);
}
上面代码通过push()方法添加项,界面没有显示添加的内容。
创建一个包含现有数组的项且末尾添加一个新项的新数组。有多种方法可以做到这一点,但最简单的方法是使用...数组展开语法:
setArtists( // Replace the state
[ // with a new array
...artists, // that contains all the old items
{ id: nextId++, name: name } // and one new item at the end
]
);
现在向数组添加项后,界面能够正确的更新显示:
import { useState } from 'react';
let nextId = 0;
export default function List() {
const [name, setName] = useState('');
const [artists, setArtists] = useState([]);
return (
<>
<h1>Inspiring sculptors:</h1>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
<button onClick={() => {
setArtists([
...artists,
{ id: nextId++, name: name }
]);
}}>Add</button>
<ul>
{artists.map(artist => (
<li key={artist.id}>{artist.name}</li>
))}
</ul>
</>
);
}
删除数组的项
从数组中删除项的最简单方法是将其过滤掉。换句话说,将生成一个不包含该项的新数组。要做到这一点,使用filter方法,例如:
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>Inspiring sculptors:</h1>
<ul>
{artists.map(artist => (
<li key={artist.id}>
{artist.name}{' '}
<button onClick={() => {
setArtists(
artists.filter(a =>
a.id !== artist.id
)
);
}}>
Delete
</button>
</li>
))}
</ul>
</>
);
}
点击删除按钮,则把对应的项删除了。下面看看事件处理函数:
setArtists(
artists.filter(a => a.id !== artist.id)
);
artists.filter(a => a.id !== artist.id)创建一个与artist.id不同的项组成的数组。filter()方法不修改原数组。点击删除按钮过滤artist数组,得到一个新数组,新数组通过设置函数赋值,然后触发重新渲染。
修改数组项
如果想修改数组的一些项,使用map()方法创建一个新数组。传递给map()方法的函数根据data 或index处理每一项。
在这个例子中,一个数组保存了两个圆和一个正方形的坐标。当你按下按钮时,圆圈向下移动50个像素。通过使用map()生成一个新的数组来实现:
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') {
// No change
return shape;
} else {
// Return a new circle 50px below
return {
...shape,
y: shape.y + 50,
};
}
});
// Re-render with the new array
setShapes(nextShapes);
}
return (
<>
<button onClick={handleClick}>
Move circles down!
</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,
}} />
))}
</>
);
}
替换数组的项
替换数组中的一个或多个项是非常常见的。arr[0] = 'bird'这样的赋值会改变原始数组,需要使用map。 要替换一个项,使用map创建一个新数组。在map调用中,接收index为第二个参数。用index决定是返回原始值(第一个参数值)还是返回一个新值。
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) {
// Increment the clicked counter
return c + 1;
} else {
// The rest haven't changed
return c;
}
});
setCounters(nextCounters);
}
return (
<ul>
{counters.map((counter, i) => (
<li key={i}>
{counter}
<button onClick={() => {
handleIncrementClick(i);
}}>+1</button>
</li>
))}
</ul>
);
}
向数组插入元素
有时想要在既不在开头也不在末尾的特定位置插入一项。可以使用...数组展开语法和slice()方法。slice()方法从数组中切出一个“切片”。想要插入一个新项,首先创建一个数组在插入点之前展开切片,然后插入新项,然后是原始数组的其余部分。
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; // Could be any index
const nextArtists = [
// Items before the insertion point:
...artists.slice(0, insertAt),
// New item:
{ id: nextId++, name: name },
// Items after the insertion point:
...artists.slice(insertAt)
];
setArtists(nextArtists);
setName('');
}
return (
<>
<h1>Inspiring sculptors:</h1>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
<button onClick={handleClick}>
Insert
</button>
<ul>
{artists.map(artist => (
<li key={artist.id}>{artist.name}</li>
))}
</ul>
</>
);
}
对数组做其他的更改
有些时候不能单独使用扩展语法和不修改原数组的方法(如map()和filter())来完成。比如,你想对一个数组排序。JavaScript的reverse()和sort()方法会改变原始数组,不能直接使用它们。
可以先复制数组,然后再对其进行更改。
例如:
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}>
Reverse
</button>
<ul>
{list.map(artwork => (
<li key={artwork.id}>{artwork.title}</li>
))}
</ul>
</>
);
}
首先用 [...list]展开语法创建原始数组的副本。现在有了一个副本,可以使用如nextList.reverse()或nextList.sort()之类的操作数组的方法,或者甚至使用nextList[0] = "something"重新赋值。
即使复制一个数组,也不能直接改变其中的现有项。这是因为复制是浅层的——新数组包含与原数组相同的项。如果在复制的数组中修改对象,就是在改变现有state。例如,像这样的代码就是一个问题。
const nextList = [...list];
nextList[0].seen = true; // Problem: mutates list[0]
setList(nextList);
虽然nextList和list是两个不同的数组,但nextList[0]和list[0]指向同一个对象。如果改变nextList[0]同时也了改变list[0],应该避免!可以用与更新嵌套JavaScript对象类似的方法来解决这个问题——复制想要更改的单个项,而不是改变它们。
更新数组object类型的项
对象并不真正位于数组的“内部”。在代码中,它们可能看起来在“内部”,实际上数组内的每个项是指向对象的指针。在更改嵌套字段(如list[0])时需要小心,另一个列表可能同时指向数组的相同元素!
在更新嵌套的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>Art Bucket List</h1>
<h2>My list of art to see:</h2>
<ItemList
artworks={myList}
onToggle={handleToggleMyList} />
<h2>Your list of art to see:</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; // Problem: mutates an existing item
setMyList(myNextList);
虽然myNextList数组是新数组,但其项与原始myList数组中的项相同。更改artwork.seen同时更改了原始数组的项,产生了一个bug。像这样的漏洞可能很难考虑,但是如果不改变state,就不会有这样的问题。
使用map将新项替换旧项,不改变原始state。
setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// Create a *new* object with changes
return { ...artwork, seen: nextSeen };
} else {
// No changes
return artwork;
}
}));
...是用于创建对象副本的对象展开语法。
使用这种方法,现有的state项都不会被改变,修复了bug:
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) {
setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// Create a *new* object with changes
return { ...artwork, seen: nextSeen };
} else {
// No changes
return artwork;
}
}));
}
function handleToggleYourList(artworkId, nextSeen) {
setYourList(yourList.map(artwork => {
if (artwork.id === artworkId) {
// Create a *new* object with changes
return { ...artwork, seen: nextSeen };
} else {
// No changes
return artwork;
}
}));
}
return (
<>
<h1>Art Bucket List</h1>
<h2>My list of art to see:</h2>
<ItemList
artworks={myList}
onToggle={handleToggleMyList} />
<h2>Your list of art to see:</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>
);
}
使用Immer简化逻辑
使用Immer能直接改变state并且Immer负责给生成副本。
下面用Immer重写艺术列表功能:
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>Art Bucket List</h1>
<h2>My list of art to see:</h2>
<ItemList
artworks={myList}
onToggle={handleToggleMyList} />
<h2>Your list of art to see:</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>
);
}
使用Immer,直接修改state对象artwork.seen = nextSeen:
updateMyTodos(draft => {
const artwork = draft.find(a => a.id === artworkId);
artwork.seen = nextSeen;
});
实际上没有修改原始的state,修改的是Immer提供的另外的一个特殊的draft对象。类似的,也可以使用push()和pop()方法修改draft。在幕后,Immer根据你对draft的改变创建新的state。这样使你的代码写起来简洁并且不改变原始的state。
参考文献: Updating Arrays in State