在 JavaScript 中,数组是可变的,但当你将它们存储在状态中时,应该将它们视为不可变的。就像对象一样,当你想更新存储在状态中的数组时,你需要创建一个新的数组(或复制一个现有的数组),然后将状态设置为使用这个新数组。
🌐 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.
你将学习到
- 如何在 React 状态下添加、删除或更改数组中的条目
- 如何更新数组内的对象
- 如何使用 Immer 减少数组复制的重复性
更新数组而不改变
🌐 Updating arrays without mutation
在 JavaScript 中,数组只是另一种对象。像使用对象一样,你应该将 React 状态中的数组视为只读。 这意味着你不应该像 arr[0] = 'bird' 那样重新分配数组中的项目,也不应该使用会修改数组的方法,例如 push() 和 pop()。
🌐 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().
相反,每次你想更新数组时,你都需要将一个新的数组传递给你的状态设置函数。为此,你可以通过调用原始数组中非修改的方法(如 filter() 和 map())来创建一个新的数组。然后,你可以将状态设置为生成的新数组。
🌐 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.
这是一个常见数组操作的参考表。在处理 React 状态中的数组时,你需要避免使用左列中的方法,而是优先使用右列中的方法:
🌐 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:
| 避免(会修改原数组) | 推荐(返回一个新数组) | |
|---|---|---|
| 添加 | push, unshift | concat, [...arr] 扩展语法 (示例) |
| 删除 | pop, shift, splice | filter, slice (示例) |
| 替换 | splice, arr[i] = ... 赋值 | map (示例) |
| 排序 | reverse, sort | 先复制数组 (示例) |
或者,你可以使用 Immer,它允许你使用两列中的方法。
🌐 Alternatively, you can use Immer which lets you use methods from both columns.
添加到数组
🌐 Adding to an array
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={() => { artists.push({ id: nextId++, name: name, }); }}>Add</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( // 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>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> </> ); }
数组展开语法也允许你通过将一个元素放在原始 ...artists 的前面来添加它:
🌐 The array spread syntax also lets you prepend an item by placing it before the original ...artists:
setArtists([
{ id: nextId++, name: name },
...artists // Put old items at the end
]);通过这种方式,spread 可以通过向数组末尾添加来完成 push() 的工作,通过向数组开头添加来完成 unshift() 的工作。可以在上方的沙盒中尝试!
🌐 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!
从数组中删除
🌐 Removing from an array
从数组中移除一个项目的最简单方法是过滤掉它。换句话说,你将生成一个不包含该项目的新数组。为此,使用 filter 方法,例如:
🌐 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:
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> </> ); }
点击几次“删除”按钮,查看它的点击处理程序。
🌐 Click the “Delete” button a few times, and look at its click handler.
setArtists(
artists.filter(a => a.id !== artist.id)
);这里,artists.filter(a => a.id !== artist.id) 意思是“创建一个数组,该数组由那些 ID 与 artist.id 不同的 artists 组成”。换句话说,每个艺术家的“删除”按钮会将那个艺术家从数组中过滤掉,然后请求使用生成的数组重新渲染。注意,filter 并不修改原始数组。
🌐 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.
转换数组
🌐 Transforming an array
如果你想更改数组中的部分或所有项,你可以使用 map() 来创建一个新的数组。你传递给 map 的函数可以根据每个项的数据或索引(或两者)来决定如何处理它。
🌐 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).
在这个例子中,一个数组保存了两个圆和一个正方形的坐标。当你按下按钮时,它只会将圆向下移动50像素。它通过使用 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():
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, }} /> ))} </> ); }
替换数组中的条目
🌐 Replacing items in an array
特别常见的情况是想替换数组中的一个或多个元素。像 arr[0] = 'bird' 这样的赋值是在改变原数组,因此你也会想使用 map 来完成这个操作。
🌐 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.
要替换一个项目,请使用 map 创建一个新数组。在你的 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:
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
有时,你可能想在某个既不是开头也不是结尾的位置插入一个元素。为此,你可以使用 ... 数组展开语法和 slice() 方法。slice() 方法允许你裁剪数组的“片段”。要插入一个元素,你将创建一个数组,先展开插入点之前的片段,然后放入新元素,最后再放入原数组的其余部分。
🌐 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.
在此示例中,插入按钮总是在索引 1 处插入:
🌐 In this example, the Insert button always inserts at the index 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> </> ); }
对数组进行其他更改
🌐 Making other changes to an array
有些操作仅使用展开语法和非修改方法如 map() 和 filter() 是无法完成的。例如,你可能想要反转或排序一个数组。JavaScript 的 reverse() 和 sort() 方法会修改原数组,因此你不能直接使用它们。
🌐 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.
然而,你可以先复制这个数组,然后再对它进行更改。
例如:
🌐 For example:
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" 来分配单个元素。
🌐 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".
然而,即使你复制了一个数组,你也不能直接修改其中现有的元素。 这是因为复制是浅拷贝——新数组将包含与原数组相同的元素。因此,如果你修改复制数组中的对象,你实际上是在修改现有的状态。例如,像这样的代码就是一个问题。
🌐 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.
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 对象的方式来解决这个问题——复制你想要更改的单个项目,而不是直接修改它们。方法如下。
🌐 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.
更新数组中的对象
🌐 Updating objects inside arrays
对象并不是真正位于数组“内部”。它们在代码中可能看起来像是“内部”的,但数组中的每个对象都是一个独立的值,数组“指向”它们。这就是为什么在修改嵌套字段如 list[0] 时需要小心的原因。别人的艺术作品列表可能指向数组的同一个元素!
🌐 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!
**在更新嵌套状态时,你需要从希望更新的点开始创建副本,并一直到顶层。**让我们看看这是如何工作的。
在这个例子中,两个独立的艺术作品列表具有相同的初始状态。它们本应是隔离的,但由于一次变更,它们的状态意外地被共享,并且在一个列表中勾选复选框会影响另一个列表:
🌐 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:
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> ); }
问题出在这样的代码中:
🌐 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);虽然 myNextList 数组本身是新的,但项目本身与原来的 myList 数组中的相同。因此更改 artwork.seen 会更改原始艺术品项目。该艺术品项目也在 yourList 中,这导致了这个错误。像这样的错误可能很难思考,但幸运的是,如果避免修改状态,它们就会消失。
🌐 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.
你可以使用 map 在不更改原有内容的情况下,用其更新版本替换旧项目。
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:
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> ); }
一般来说,你应该只修改你刚创建的对象。 如果你正在插入一件新的艺术品,你可以修改它,但如果你处理的是已经存在于状态中的对象,你需要先复制一份。
🌐 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.
使用 Immer 编写简洁的更新逻辑
🌐 Write concise update logic with Immer
在不进行修改的情况下更新嵌套数组可能会有点重复。就像处理对象一样:
🌐 Updating nested arrays without mutation can get a little bit repetitive. Just as with objects:
- 通常,你不需要更新比几层更深的状态。如果你的状态对象非常深,你可能想要以不同方式重构它们,使它们扁平化。
- 如果你不想更改你的状态结构,你可能更愿意使用 Immer,它让你可以使用方便但会修改的语法,并为你处理生成副本的工作。
这是用 Immer 重写的艺术遗愿清单示例:
🌐 Here is the Art Bucket List example rewritten with 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": {} }
注意使用 Immer 后,像 artwork.seen = nextSeen 这样的变更现在是可以的:
🌐 Note how with Immer, mutation like artwork.seen = nextSeen is now okay:
updateMyTodos(draft => {
const artwork = draft.find(a => a.id === artworkId);
artwork.seen = nextSeen;
});这是因为你没有更改_原始_状态,而是更改了 Immer 提供的一个特殊 draft 对象。类似地,你可以对 draft 的内容应用像 push() 和 pop() 这样的修改方法。
🌐 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.
在幕后,Immer 总是根据你对 draft 所做的更改从头构建下一个状态。这使你的事件处理程序保持非常简洁,同时永远不会改变状态。
🌐 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.
回顾
- 你可以将数组放入状态,但不能更改它们。
- 不要对数组进行修改,而是创建它的新版本,并将状态更新为它。
- 你可以使用
[...arr, newItem]数组展开语法来创建包含新项的数组。 - 你可以使用
filter()和map()来创建包含经过筛选或转换的元素的新数组。 - 你可以使用 Immer 来保持代码简洁。
挑战 1 of 4: 更新购物车中的商品
🌐 Update an item in the shopping cart
填写 handleIncreaseClick 逻辑,以便按下“+”时增加相应的数字:
🌐 Fill in the handleIncreaseClick logic so that pressing ”+” increases the corresponding number:
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> ); }