Arrays are mutable in JavaScript, but you should treat them as immutable when you store them in state. Just like with objects, when you want to update an array stored in state, you need to create a new one (or make a copy of an existing one), and then set state to use the new array. 数组是另外一种可以存储在 state 中的 JavaScript 对象,它虽然是可变的,但是却应该被视为不可变。同对象一样,当你想要更新存储于 state 中的数组时,你需要创建一个新的数组(或者创建一份已有数组的拷贝值),并使用新数组设置 state。
你将会学习到
- How to add, remove, or change items in an array in React state
- 如何添加、删除或者修改 React state 中的数组中的元素
- How to update an object inside of an array
- 如何更新数组内部的对象
- How to make array copying less repetitive with Immer
- 如何通过 Immer 降低数组拷贝的重复度
在没有 mutation 的前提下更新数组 | Updating arrays without mutation
In JavaScript, arrays are just another kind of object. Like with objects, you should treat arrays in React state as read-only. This means that you shouldn’t reassign items inside an array like arr[0] = 'bird'
, and you also shouldn’t use methods that mutate the array, such as push()
and pop()
.
在 JavaScript 中,数组只是另一种对象。同对象一样,你需要将 React state 中的数组视为只读的。这意味着你不应该使用类似于 arr[0] = 'bird'
这样的方式来重新分配数组中的元素,也不应该使用会直接修改原始数组的方法,例如 push()
和 pop()
。
Instead, every time you want to update an array, you’ll want to pass a new array to your state setting function. To do that, you can create a new array from the original array in your state by calling its non-mutating methods like filter()
and map()
. Then you can set your state to the resulting new array.
相反,每次要更新一个数组时,你需要把一个新的数组传入 state 的 setting 方法中。为此,你可以通过使用像 filter()
和 map()
这样不会直接修改原始值的方法,从原始数组生成一个新的数组。然后你就可以将 state 设置为这个新生成的数组。
Here is a reference table of common array operations. When dealing with arrays inside React state, you will need to avoid the methods in the left column, and instead prefer the methods in the right column: 下面是常见数组操作的参考表。当你操作 React state 中的数组时,你需要避免使用左列的方法,而首选右列的方法:
avoid (mutates the array) 避免使用 (会改变原始数组) | prefer (returns a new array) 推荐使用(会返回一个新数组) | |
---|---|---|
adding 添加元素 | push ,unshift | concat , [...arr] spread syntax (example)concat ,[...arr] 展开语法(例子) |
removing 删除元素 | pop ,shift ,splice | filter , slice (example)filter ,slice (例子) |
replacing 替换元素 | splice , arr[i] = ... assignmentsplice ,arr[i] = ... 赋值 | map (example)map (例子) |
sorting 排序 | reverse ,sort | copy the array first (example) 先将数组复制一份(例子) |
Alternatively, you can use Immer which lets you use methods from both columns. 或者,你可以使用 Immer ,这样你便可以使用表格中的所有方法了。
向数组中添加元素 | Adding to an array
push()
will mutate an array, which you don’t want:
push()
会直接修改原始数组,而你不希望这样:
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={() => { artists.push({ id: nextId++, name: name, }); }}>添加</button> <ul> {artists.map(artist => ( <li key={artist.id}>{artist.name}</li> ))} </ul> </> ); }
Instead, create a new array which contains the existing items and a new item at the end. There are multiple ways to do this, but the easiest one is to use the ...
array spread syntax:
相反,你应该创建一个 新 数组,其包含了原始数组的所有元素 以及 一个在末尾的新元素。这可以通过很多种方法实现,最简单的一种就是使用 ...
数组展开 语法:
setArtists( // 替换 state // 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
]
);
Now it works correctly: 现在代码可以正常运行了:
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> </> ); }
The array spread syntax also lets you prepend an item by placing it before the original ...artists
:
数组展开运算符还允许你把新添加的元素放在原始的 ...artists
之前:
setArtists([
{ id: nextId++, name: name },
...artists // 将原数组中的元素放在末尾 // Put old items at the end
]);
In this way, spread can do the job of both push()
by adding to the end of an array and unshift()
by adding to the beginning of an array. Try it in the sandbox above!
这样一来,展开操作就可以完成 push()
和 unshift()
的工作,将新元素添加到数组的末尾和开头。你可以在上面的 sandbox 中尝试一下!
从数组中删除元素 | Removing from an array
The easiest way to remove an item from an array is to filter it out. In other words, you will produce a new array that will not contain that item. To do this, use the filter
method, for example:
从数组中删除一个元素最简单的方法就是将它过滤出去。换句话说,你需要生成一个不包含该元素的新数组。这可以通过 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>振奋人心的雕塑家们:</h1> <ul> {artists.map(artist => ( <li key={artist.id}> {artist.name}{' '} <button onClick={() => { setArtists( artists.filter(a => a.id !== artist.id ) ); }}> 删除 </button> </li> ))} </ul> </> ); }
Click the “Delete” button a few times, and look at its click handler. 点击“删除”按钮几次,并且查看按钮处理点击事件的代码。
setArtists(
artists.filter(a => a.id !== artist.id)
);
Here, artists.filter(a => a.id !== artist.id)
means “create an array that consists of those artists
whose IDs are different from artist.id
”. In other words, each artist’s “Delete” button will filter that artist out of the array, and then request a re-render with the resulting array. Note that filter
does not modify the original array.
这里,artists.filter(s => s.id !== artist.id)
表示“创建一个新的数组,该数组由那些 ID 与 artists.id
不同的 artists
组成”。换句话说,每个 artist 的“删除”按钮会把 那一个 artist 从原始数组中过滤掉,并使用过滤后的数组再次进行渲染。注意,filter
并不会改变原始数组。
转换数组 | Transforming an array
If you want to change some or all items of the array, you can use map()
to create a new array. The function you will pass to map
can decide what to do with each item, based on its data or its index (or both).
如果你想改变数组中的某些或全部元素,你可以用 map()
创建一个新数组。你传入 map
的函数决定了要根据每个元素的值或索引(或二者都要)对元素做何处理。
In this example, an array holds coordinates of two circles and a square. When you press the button, it moves only the circles down by 50 pixels. It does this by producing a new array of data using map()
:
在下面的例子中,一个数组记录了两个圆形和一个正方形的坐标。当你点击按钮时,仅有两个圆形会向下移动 100 像素。这是通过使用 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 // 返回一个新的圆形,位置在下方 50px 处 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, }} /> ))} </> ); }
替换数组中的元素 | Replacing items in an array
It is particularly common to want to replace one or more items in an array. Assignments like arr[0] = 'bird'
are mutating the original array, so instead you’ll want to use map
for this as well.
想要替换数组中一个或多个元素是非常常见的。类似 arr[0] = 'bird'
这样的赋值语句会直接修改原始数组,所以在这种情况下,你也应该使用 map
。
To replace an item, create a new array with map
. Inside your map
call, you will receive the item index as the second argument. Use it to decide whether to return the original item (the first argument) or something else:
要替换一个元素,请使用 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> ); }
向数组中插入元素 | Inserting into an array
Sometimes, you may want to insert an item at a particular position that’s neither at the beginning nor at the end. To do this, you can use the ...
array spread syntax together with the slice()
method. The slice()
method lets you cut a “slice” of the array. To insert an item, you will create an array that spreads the slice before the insertion point, then the new item, and then the rest of the original array.
有时,你也许想向数组特定位置插入一个元素,这个位置既不在数组开头,也不在末尾。为此,你可以将数组展开运算符 ...
和 slice()
方法一起使用。slice()
方法让你从数组中切出“一片”。为了将元素插入数组,你需要先展开原数组在插入点之前的切片,然后插入新元素,最后展开原数组中剩下的部分。
In this example, the Insert button always inserts at the index 1
:
下面的例子中,插入按钮总是会将元素插入到数组中索引为 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>振奋人心的雕塑家们:</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> </> ); }
其他改变数组的情况 | Making other changes to an array
There are some things you can’t do with the spread syntax and non-mutating methods like map()
and filter()
alone. For example, you may want to reverse or sort an array. The JavaScript reverse()
and sort()
methods are mutating the original array, so you can’t use them directly.
总会有一些事,是你仅仅依靠展开运算符和 map()
或者 filter()
等不会直接修改原值的方法所无法做到的。例如,你可能想翻转数组,或是对数组排序。而 JavaScript 中的 reverse()
和 sort()
方法会改变原数组,所以你无法直接使用它们。
However, you can copy the array first, and then make changes to it. 然而,你可以先拷贝这个数组,再改变这个拷贝后的值。
例如:
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}> 翻转 </button> <ul> {list.map(artwork => ( <li key={artwork.id}>{artwork.title}</li> ))} </ul> </> ); }
Here, you use the [...list]
spread syntax to create a copy of the original array first. Now that you have a copy, you can use mutating methods like nextList.reverse()
or nextList.sort()
, or even assign individual items with nextList[0] = "something"
.
在这段代码中,你先使用 [...list]
展开运算符创建了一份数组的拷贝值。当你有了这个拷贝值后,你就可以使用像 nextList.reverse()
或 nextList.sort()
这样直接修改原数组的方法。你甚至可以通过 nextList[0] = "something"
这样的方式对数组中的特定元素进行赋值。
However, even if you copy an array, you can’t mutate existing items inside of it directly. This is because copying is shallow—the new array will contain the same items as the original one. So if you modify an object inside the copied array, you are mutating the existing state. For example, code like this is a problem. 然而,即使你拷贝了数组,你还是不能直接修改其内部的元素。这是因为数组的拷贝是浅拷贝——新的数组中依然保留了与原始数组相同的元素。因此,如果你修改了拷贝数组内部的某个对象,其实你正在直接修改当前的 state。举个例子,像下面的代码就会带来问题。
const nextList = [...list];
nextList[0].seen = true; // 问题:直接修改了 list[0] 的值 // Problem: mutates list[0]
setList(nextList);
Although nextList
and list
are two different arrays, nextList[0]
and list[0]
point to the same object. So by changing nextList[0].seen
, you are also changing list[0].seen
. This is a state mutation, which you should avoid! You can solve this issue in a similar way to updating nested JavaScript objects—by copying individual items you want to change instead of mutating them. Here’s how.
虽然 nextList
和 list
是两个不同的数组,nextList[0]
和 list[0]
却指向了同一个对象。因此,通过改变 nextList[0].seen
,list[0].seen
的值也被改变了。这是一种 state 的 mutation 操作,你应该避免这么做!你可以用类似于 更新嵌套的 JavaScript 对象 的方式解决这个问题——拷贝想要修改的特定元素,而不是直接修改它。下面是具体的操作。
更新数组内部的对象 | Updating objects inside arrays
Objects are not really located “inside” arrays. They might appear to be “inside” in code, but each object in an array is a separate value, to which the array “points”. This is why you need to be careful when changing nested fields like list[0]
. Another person’s artwork list may point to the same element of the array!
对象并不是 真的 位于数组“内部”。可能他们在代码中看起来像是在数组“内部”,但其实数组中的每个对象都是这个数组“指向”的一个存储于其它位置的值。这就是当你在处理类似 list[0]
这样的嵌套字段时需要格外小心的原因。其他人的艺术品清单可能指向了数组的同一个元素!
When updating nested state, you need to create copies from the point where you want to update, and all the way up to the top level. Let’s see how this works. 当你更新一个嵌套的 state 时,你需要从想要更新的地方创建拷贝值,一直这样,直到顶层。 让我们看一下这该怎么做。
In this example, two separate artwork lists have the same initial state. They are supposed to be isolated, but because of a mutation, their state is accidentally shared, and checking a box in one list affects the other list: 在下面的例子中,两个不同的艺术品清单有着相同的初始 state。他们本应该互不影响,但是因为一次 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>艺术愿望清单</h1> <h2>我想看的艺术清单:</h2> <ItemList artworks={myList} onToggle={handleToggleMyList} /> <h2>你想看的艺术清单:</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> ); }
The problem is in code like this: 问题出在下面这段代码中:
const myNextList = [...myList];
const artwork = myNextList.find(a => a.id === artworkId);
artwork.seen = nextSeen; // 问题:直接修改了已有的元素 // Problem: mutates an existing item
setMyList(myNextList);
Although the myNextList
array itself is new, the items themselves are the same as in the original myList
array. So changing artwork.seen
changes the original artwork item. That artwork item is also in yourList
, which causes the bug. Bugs like this can be difficult to think about, but thankfully they disappear if you avoid mutating state.
虽然 myNextList
这个数组是新的,但是其内部的元素本身与原数组 myList
是相同的。因此,修改 artwork.seen
,其实是在修改原始的 artwork 对象。而这个 artwork 对象也被 yourList
使用,这样就带来了 bug。这样的 bug 可能难以想到,但好在如果你避免直接修改 state,它们就会消失。
You can use map
to substitute an old item with its updated version without mutation.
你可以使用 map
在没有 mutation 的前提下将一个旧的元素替换成更新的版本。
setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// Create a *new* object with changes
// 创建包含变更的*新*对象
return { ...artwork, seen: nextSeen };
} else {
// No changes
// 没有变更
return artwork;
}
}));
Here, ...
is the object spread syntax used to create a copy of an object.
此处的 ...
是一个对象展开语法,被用来创建一个对象的拷贝.
With this approach, none of the existing state items are being mutated, and the bug is fixed: 通过这种方式,没有任何现有的 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>艺术愿望清单</h1> <h2>我想看的艺术清单:</h2> <ItemList artworks={myList} onToggle={handleToggleMyList} /> <h2>你想看的艺术清单:</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> ); }
In general, you should only mutate objects that you have just created. If you were inserting a new artwork, you could mutate it, but if you’re dealing with something that’s already in state, you need to make a copy. 通常来讲,你应该只直接修改你刚刚创建的对象。如果你正在插入一个新的 artwork,你可以修改它,但是如果你想要改变的是 state 中已经存在的东西,你就需要先拷贝一份了。
使用 Immer 编写简洁的更新逻辑 | Write concise update logic with Immer
Updating nested arrays without mutation can get a little bit repetitive. Just as with objects: 在没有 mutation 的前提下更新嵌套数组可能会变得有点重复。就像对对象一样:
- Generally, you shouldn’t need to update state more than a couple of levels deep. If your state objects are very deep, you might want to restructure them differently so that they are flat.
- 通常情况下,你应该不需要更新处于非常深层级的 state 。如果你有此类需求,你或许需要调整一下数据的结构,让数据变得扁平一些。
- If you don’t want to change your state structure, you might prefer to use Immer, which lets you write using the convenient but mutating syntax and takes care of producing the copies for you.
- 如果你不想改变 state 的数据结构,你也许会更喜欢使用 Immer ,它让你可以继续使用方便的,但会直接修改原值的语法,并负责为你生成拷贝值。
Here is the Art Bucket List example rewritten with Immer: 下面是我们用 Immer 来重写的艺术愿望清单的例子:
{ "dependencies": { "immer": "1.7.3", "react": "latest", "react-dom": "latest", "react-scripts": "latest", "use-immer": "0.5.1" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, "devDependencies": {} }
Note how with Immer, mutation like artwork.seen = nextSeen
is now okay:
请注意当使用 Immer 时,类似 artwork.seen = nextSeen
这种会产生 mutation 的语法不会再有任何问题了:
updateMyTodos(draft => {
const artwork = draft.find(a => a.id === artworkId);
artwork.seen = nextSeen;
});
This is because you’re not mutating the original state, but you’re mutating a special draft
object provided by Immer. Similarly, you can apply mutating methods like push()
and pop()
to the content of the draft
.
这是因为你并不是在直接修改原始的 state,而是在修改 Immer 提供的一个特殊的 draft
对象。同理,你也可以为 draft
的内容使用 push()
和 pop()
这些会直接修改原值的方法。
Behind the scenes, Immer always constructs the next state from scratch according to the changes that you’ve done to the draft
. This keeps your event handlers very concise without ever mutating state.
在幕后,Immer 总是会根据你对 draft
的修改来从头开始构建下一个 state。这使得你的事件处理程序非常的简洁,同时也不会直接修改 state。
摘要
- You can put arrays into state, but you can’t change them.
- 你可以把数组放入 state 中,但你不应该直接修改它。
- Instead of mutating an array, create a new version of it, and update the state to it.
- 不要直接修改数组,而是创建它的一份 新的 拷贝,然后使用新的数组来更新它的状态。
- You can use the
[...arr, newItem]
array spread syntax to create arrays with new items. - 你可以使用
[...arr, newItem]
这样的数组展开语法来向数组中添加元素。 - You can use
filter()
andmap()
to create new arrays with filtered or transformed items. - 你可以使用
filter()
和map()
来创建一个经过过滤或者变换的数组。 - You can use Immer to keep your code concise.
- 你可以使用 Immer 来保持代码简洁。
第 1 个挑战 共 4 个挑战: 更新购物车中的商品 | Update an item in the shopping cart
Fill in the handleIncreaseClick
logic so that pressing ”+” increases the corresponding number:
填写 handleIncreaseClick
的逻辑,以便按下“+”时递增对应数字:
import { useState } from 'react'; const initialProducts = [{ id: 0, name: 'Baklava', count: 1, }, { id: 1, name: 'Cheese', count: 5, }, { id: 2, name: 'Spaghetti', count: 2, }]; export default function ShoppingCart() { const [ products, setProducts ] = useState(initialProducts) function handleIncreaseClick(productId) { } return ( <ul> {products.map(product => ( <li key={product.id}> {product.name} {' '} (<b>{product.count}</b>) <button onClick={() => { handleIncreaseClick(product.id); }}> + </button> </li> ))} </ul> ); }