v19.2Latest

更新 state 中的数组

在 JavaScript 中,数组是可变的,但当你将它们存储在 state 中时,应该将其视为不可变的。就像对象一样,当你想要更新存储在 state 中的数组时,你需要创建一个新的数组(或复制一个现有的数组),然后将 state 设置为使用这个新数组。

您将学习
  • 如何在 React state 中添加、移除或更改数组中的项目
  • 如何更新数组内的对象
  • 如何使用 Immer 减少数组复制的重复性

避免直接修改数组

在 JavaScript 中,数组只是另一种对象。与对象一样你应该将 React state 中的数组视为只读的。这意味着你不应该像 arr[0] = 'bird'这样重新分配数组内的项目,也不应该使用会改变数组的方法,例如push()pop()

相反,每次你想要更新数组时,都应该向你的 state 设置函数传递一个新的数组。为此,你可以通过调用其非变异方法(如 filter()map())从 state 中的原始数组创建一个新数组。然后,你可以将你的 state 设置为这个结果新数组。

以下是常见数组操作的参考表。当处理 React state 中的数组时,你需要避免使用左列的方法,而应优先使用右列的方法:

避免(会改变数组)推荐(返回新数组)
添加push,unshiftconcat,[...arr]展开语法(示例
移除pop,shift,splicefilter,slice示例
替换splice,arr[i] = ... 赋值map示例
排序reverse,sort先复制数组(示例

或者,你可以使用 Immer,它允许你使用两列中的方法。

陷阱

不幸的是,slicesplice 名称相似但差异很大:

  • slice允许你复制数组或其一部分。
  • splice会改变数组(用于插入或删除项目)。

在 React 中,你会更频繁地使用slice(没有p!),因为你不想改变 state 中的对象或数组。更新对象解释了什么是改变以及为什么不建议在 state 中使用它。

向数组添加元素

push()会改变数组,这是你不希望发生的:

相反,创建一个新的数组,其中包含现有项目以及末尾的一个新项目。有多种方法可以实现,但最简单的是使用...数组展开语法:

现在它可以正确工作了:

数组展开语法也允许你将一个项目放在原始...artists之前来将其前置:

这样,展开语法既可以完成push()在数组末尾添加项目的任务,也可以完成unshift()在数组开头添加项目的任务。在上面的沙盒中试试吧!

从数组中移除项目

从数组中移除项目最简单的方法是将其过滤掉。换句话说,你将生成一个不包含该项目的新数组。为此,请使用filter方法,例如:

点击几次“Delete”按钮,并查看其点击处理程序。

这里,artists.filter(a => a.id !== artist.id)的意思是“创建一个由那些ID与artists组成的数组”。换句话说,每个艺术家的“Delete”按钮都会将artist.id不同的艺术家从数组中过滤掉,然后请求使用结果数组重新渲染。请注意filter不会修改原始数组。

转换数组

如果你想更改数组中的部分或全部项目,可以使用map()来创建一个新的数组。你传递给map的函数可以根据每个项目的数据或其索引(或两者)来决定如何处理每个项目。

在这个例子中,一个数组保存了两个圆和一个正方形的坐标。当你按下按钮时,它只将圆向下移动50像素。这是通过使用map()生成一个新的数据数组来实现的:

替换数组中的项目

替换数组中的一个或多个项目尤其常见。像arr[0] = 'bird'这样的赋值会改变原始数组,因此你也应该为此使用map

要替换一个项目,请使用map创建一个新数组。在你的map调用内部,你将收到项目索引作为第二个参数。用它来决定是返回原始项目(第一个参数)还是其他内容:

向数组中插入元素

有时,你可能希望将元素插入到特定位置,该位置既不在开头也不在结尾。为此,你可以使用...数组展开语法配合slice()方法。slice()方法允许你“切取”数组的一部分。要插入元素,你需要创建一个数组,该数组在插入点之前展开切片,然后是新元素,最后是原始数组的其余部分。

在此示例中,“插入”按钮始终在索引1处插入:

对数组进行其他更改

有些操作仅靠展开语法和map()filter()等非变异方法是无法完成的。例如,你可能希望反转或排序数组。JavaScript 的reverse()sort()方法会改变原始数组,因此你不能直接使用它们。

但是,你可以先复制数组,然后再对其进行更改。

例如:

这里,你使用[...list]展开语法首先创建原始数组的副本。现在你有了副本,就可以使用nextList.reverse()nextList.sort()等变异方法,甚至可以通过nextList[0] = "something"来分配单个元素。

但是,即使你复制了数组,也不能直接改变其中现有的元素。内部元素。这是因为复制是浅层的——新数组将包含与原始数组相同的元素。因此,如果你修改复制数组中的对象,就是在改变现有的状态。例如,这样的代码是有问题的。

尽管nextListlist是两个不同的数组,但nextList[0]list[0]指向同一个对象。因此,更改nextList[0].seen也会更改list[0].seen。这是一种状态突变,你应该避免!你可以通过类似于更新嵌套 JavaScript 对象的方式来解决这个问题——复制你想要更改的单个元素,而不是改变它们。具体做法如下。

更新数组中的对象

对象并非真正“位于”数组内部。它们在代码中可能看起来是“在里面”,但数组中的每个对象都是一个独立的值,数组“指向”这些值。这就是为什么在更改像list[0]这样的嵌套字段时需要小心。其他人的艺术品列表可能指向数组的同一个元素!

更新嵌套状态时,你需要从你想要更新的位置开始创建副本,并一直向上复制到顶层。让我们看看这是如何工作的。

在此示例中,两个独立的艺术品列表具有相同的初始状态。它们本应是隔离的,但由于突变,它们的状态意外地共享了,在一个列表中勾选复选框会影响另一个列表:

问题出在类似这样的代码中:

虽然 myNextList数组本身是新的,但其中的元素本身 与原始 myList数组中的元素是相同的。因此,修改artwork.seen 会改变 原始的artwork 项。该 artwork 项也存在于yourList中,这就导致了 bug。这类 bug 可能难以思考,但幸运的是,如果你避免直接修改状态,它们就会消失。

你可以使用map来替换旧项为其更新后的版本,而无需直接修改。

这里的... 是对象展开语法,用于 创建对象的副本。

使用这种方法,没有任何现有的状态项被修改,bug 也就被修复了:

一般来说,你只应该修改你刚刚创建的对象。如果你要插入一个新的artwork,你可以修改它,但如果你处理的是已经存在于状态中的东西,你就需要制作一个副本。

使用 Immer 编写简洁的更新逻辑

在不直接修改的情况下更新嵌套数组可能会变得有些重复。就像处理对象时一样

  • 通常,你不需要更新超过几层深度的状态。如果你的状态对象非常深,你可能需要 以不同的方式重构它们,使其变得扁平。
  • 如果你不想改变你的状态结构,你可能更喜欢使用 Immer,它允许你使用方便但会直接修改的语法进行编写,并负责为你生成副本。

以下是使用 Immer 重写的 Art Bucket List 示例:

请注意,使用 Immer 时,artwork.seen = nextSeen这样的突变现在是可行的:

这是因为你并没有改变 原始的状态,而是改变了 Immer 提供的一个特殊的draft 对象。同样地,你也可以对 draft的内容应用诸如push()pop()这样的突变方法。

在幕后,Immer 总是根据你对 draft所做的更改,从头开始构建下一个状态。这使得你的事件处理程序非常简洁,且永远不会改变状态。

回顾

  • 你可以将数组放入状态,但不能改变它们。
  • 不要改变数组,而是创建一个数组的版本,并将状态更新为该版本。
  • 你可以使用[...arr, newItem]数组展开语法来创建包含新项的数组。
  • 你可以使用filter()map() 来创建经过筛选或转换的新数组。
  • 你可以使用 Immer 来保持代码简洁。

Try out some challenges

Challenge 1 of 4:Update an item in the shopping cart #

Fill in the handleIncreaseClick logic so that pressing ”+” increases the corresponding number: