在状态中更新数组
数组在 JavaScript 中是可变的,但在将它们存储在状态中时,您应该将它们视为不可变的。 就像对象一样,当你想更新存储在状态中的数组时,你需要创建一个新数组(或复制现有数组),然后设置状态以使用新数组。
你将学习
- 如何在 React 状态下添加、删除或更改数组中的项目
- 如何更新数组内的对象
- 如何使用 Immer 减少数组复制的重复性
更新数组而不改变老数组
在 JavaScript 中,数组只是另一种对象。 与对象一样,您应该将处于 React 状态的数组视为只读。 这意味着你不应该像 arr[0] = 'bird' 这样重新分配数组中的项目,你也不应该使用改变数组的方法,比如 push() 和 pop()。
相反,每次你想更新一个数组时,你都需要将一个新数组传递给你的状态设置函数。 为此,您可以通过调用其非修改方法(如 filter() 和 map())从您状态下的原始数组创建一个新数组。 然后您可以将您的状态设置为生成的新数组。
这里有一个常见数组操作的参考表。 在处理 React 状态中的数组时,您需要避免使用左栏中的方法,而尽量用右栏中的方法:
避免(改变数组) | 尽量(返回新数组) | |
---|---|---|
添加 | push,unshift | concat, [...arr]展开语法 |
删除 | pop, shift, splice | filter, slice |
替换 | splice, arr[i] = ... 赋值 | map |
排序 | reverse, sort | 先复制数组 |
或者,您可以使用 Immer,它允许您使用来自两列的方法。
陷阱
不幸的是, slice 和 splice 的命名相似但有很大不同:
- slice 允许您复制一个数组或其中的一部分。
- splice 改变数组(插入或删除项目)。
在 React 中,你会更频繁地使用 slice(没p!),因为你不想改变状态中的对象或数组。 更新对象解释了什么是突变以及为什么不建议将其用于状态。
添加到数组
push() 会改变数组,这不是你想要的:
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={() => {
setName('');
artists.push({
id: nextId++,
name: name,
});
}}>Add</button>
<ul>
{artists.map(artist => (
<li key={artist.id}>{artist.name}</li>
))}
</ul>
</>
);
}
相反,创建一个新数组,其中包含现有项目和末尾的新项目。 有多种方法可以做到这一点,但最简单的方法是使用 ... 数组展开语法:
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={() => {
setName('');
setArtists([
...artists,
{ id: nextId++, name: name }
]);
}}>Add</button>
<ul>
{artists.map(artist => (
<li key={artist.id}>{artist.name}</li>
))}
</ul>
</>
);
}
数组展开语法还允许您通过将项目放在原始 ...artists 之前来预先添加项目:
setArtists([
{ id: nextId++, name: name },
...artists // Put old items at the end
]);
通过这种方式,展开可以通过添加到数组的末尾来完成 push() 的工作,也可以通过添加到数组的开头来完成 unshift() 的工作。
从数组移除
从数组中删除项目的最简单方法是将其过滤掉。 换句话说,您将生成一个不包含该项目的新数组。 为此,请使用 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) 的意思是“创建一个数组,其中包含那些 ID 与 artist.id 不同的艺术家”。 换句话说,每个艺术家的“删除”按钮都会将该艺术家从数组中过滤掉,然后请求使用结果数组重新渲染。 请注意,过滤器不会修改原始数组。
数组转换
如果要更改数组的部分或全部项目,可以使用 map() 创建一个新数组。 您将传递给 map 的函数可以根据其数据或索引(或两者)决定如何处理每个项目。
在此示例中,数组包含两个圆和一个正方形的坐标。 当您按下按钮时,它只会将圆圈向下移动 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调用中,您将收到项目索引作为第二个参数。 用它来决定是否返回原始项目(第一个参数)或其他东西:
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() 方法可让您切割数组的“切片”。 要插入一个项目,您将创建一个数组,该数组展开插入点之前的切片,然后是新项目,然后是原始数组的其余部分。
在此示例中,“插入”按钮始终在索引 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; // 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';
let nextId = 3;
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" 分配单个项目。
然而,即使你复制了一个数组,你也不能直接改变其中的现有项。 这是因为复制是浅层的——新数组将包含与原始数组相同的项。 因此,如果您修改复制数组中的对象,就会改变现有状态。 例如,像这样的代码就是一个问题。
const nextList = [...list];
nextList[0].seen = true; // Problem: mutates list[0]
setList(nextList);
nextList和list虽然是两个不同的数组,但是nextList[0]和list[0]指向同一个对象。 因此,通过更改 nextList[0].seen,您也会更改 list[0].seen。 这是状态修改,您应该避免! 您可以通过与更新嵌套 JavaScript 对象类似的方式解决此问题——通过复制您想要更改的单个项目而不是改变它们。 就是这样。
更新数组中的对象
对象并不真正位于数组“内部”。 它们可能看起来在代码中“内部”,但数组中的每个对象都是一个单独的值,数组“指向”。 这就是为什么在更改像 list[0] 这样的嵌套字段时需要小心的原因。 另一个人的作品列表可能指向数组的同一个元素!
更新嵌套状态时,您需要从要更新的点开始创建副本,一直到顶层。 让我们看看这是如何工作的。
在此示例中,两个单独的艺术品列表具有相同的初始状态。 它们应该是隔离的,但由于突变,它们的状态被意外共享,选中一个列表中的框会影响另一个列表:
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 会更改原始艺术品项目。 该艺术品项目也在您的艺术品中,这会导致该错误。 像这样的错误可能很难考虑,但值得庆幸的是,如果你避免改变状态,它们就会消失。
您可以使用 map 将旧项目替换为其更新版本而无需修改。
setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// Create a *new* object with changes
return { ...artwork, seen: nextSeen };
} else {
// No changes
return artwork;
}
});
这里,... 是用于创建对象副本的对象展开语法。
使用这种方法,现有状态项都不会发生变化,并且错误已修复:
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,它可以让你使用方便但可变的语法进行编写,并负责为你生成副本。
这是用 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 [yourArtworks, 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={yourArtworks}
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,像 artwork.seen = nextSeen 这样的修改现在是可以的:
updateMyTodos(draft => {
const artwork = draft.find(a => a.id === artworkId);
artwork.seen = nextSeen;
});
这是因为您没有改变原始状态,而是改变了 Immer 提供的特殊草稿对象。 同样,您可以将 push() 和 pop() 等变异方法应用于草稿的内容。
在幕后,Immer 总是根据您对草稿所做的更改从头开始构建下一个状态。 这使您的事件处理程序非常简洁,而不会改变状态。
回顾
- 您可以将数组放入状态,但不能更改它们。
- 与其改变数组,不如创建它的新版本,并更新它的状态。
- 您可以使用 [...arr, newItem] 数组展开语法来创建包含新项的数组。
- 您可以使用 filter() 和 map() 创建包含过滤或转换项目的新数组。
- 您可以使用 Immer 来保持代码简洁。
网友评论