更新 state 中的对象
state 可以存放任何类型的 JavaScript 值,包括对象。但是你不应该直接修改存放在 React state 中的对象。相反,当你想要更新一个对象时,你需要创建一个新的对象(或者创建一个现有对象的副本),然后将 state 设置为使用这个副本。
您将学习
- 如何正确更新 React state 中的对象
- 如何在不改变原对象的情况下更新嵌套对象
- 什么是不可变性,以及如何不破坏它
- 如何使用 Immer 减少对象复制的重复性
什么是 mutation?
你可以在 state 中存放任何类型的 JavaScript 值。
到目前为止,你一直在处理数字、字符串和布尔值。这些类型的 JavaScript 值是“不可变的”,意味着不可更改或“只读”。你可以触发一次重新渲染来替换一个值:
statex 从 0变为了5,但 数字 0 本身并没有改变。在 JavaScript 中,无法对像数字、字符串和布尔值这样的内置原始值进行任何更改。
现在考虑 state 中的一个对象:
从技术上讲,可以更改 对象本身的内容。这被称为 mutation:
然而,尽管 React state 中的对象在技术上是可变的,但你应该将它们视为不可变的——就像数字、布尔值和字符串一样。你不应该改变它们,而应该总是替换它们。
将 state 视为只读
换句话说,你应该将放入 state 的任何 JavaScript 对象都视为只读的。
这个示例在 state 中存放了一个对象来表示当前指针位置。当你触摸或将光标移动到预览区域上方时,红点应该移动。但红点停留在初始位置:
问题出在这段代码上。
这段代码修改了分配给 position的对象,该对象来自上一次渲染。但由于没有使用 state 设置函数,React 并不知道对象已经改变。因此 React 没有做出任何响应。这就像在已经吃完饭后试图更改订单。虽然在某些情况下改变 state 可能有效,但我们不推荐这样做。你应该将渲染中可以访问的 state 值视为只读的。
要在此情况下实际触发重新渲染,需要创建一个新的对象并将其传递给 state 设置函数:
通过setPosition,你告诉 React:
- 用这个新对象替换
position - 并再次渲染此组件
请注意,现在当你触摸或将鼠标悬停在预览区域上时,红点如何跟随你的指针:
使用展开语法复制对象
在前面的例子中,position对象总是根据当前光标位置全新创建的。但通常,你会希望将现有的数据作为你正在创建的新对象的一部分包含进去。例如,你可能只想更新表单中的一个字段,但保留所有其他字段的先前值。
这些输入字段不起作用,因为onChange处理函数突变了状态:
例如,这行代码突变了来自过去渲染的状态:
获得你想要的行为的可靠方法是创建一个新对象并将其传递给setPerson。但在这里,你还希望将现有数据复制到其中,因为只有一个字段发生了变化:
你可以使用...对象展开语法,这样你就不需要单独复制每个属性。
现在表单可以工作了!
注意,你并没有为每个输入字段声明单独的状态变量。对于大型表单,将所有数据分组到一个对象中非常方便——只要你正确地更新它!
请注意,...展开语法是“浅层”的——它只复制一层深度。这使得它很快,但也意味着如果你想更新嵌套属性,你必须多次使用它。
更新嵌套对象
考虑如下嵌套对象结构:
如果你想更新 person.artwork.city,使用可变的方式很清楚如何操作:
但在 React 中,你需要将状态视为不可变的!为了改变city,你首先需要生成新的 artwork 对象(用之前的数据预先填充),然后生成指向新 artwork 的新 person对象:
或者,写成单个函数调用:
这有点冗长,但在许多情况下都能正常工作:
使用 Immer 编写简洁的更新逻辑
如果你的状态嵌套很深,你可能需要考虑将其扁平化。但是,如果你不想改变状态结构,你可能更喜欢一种嵌套展开的快捷方式。Immer是一个流行的库,它允许你使用方便但可变(mutating)的语法编写代码,并负责为你生成副本。使用 Immer,你编写的代码看起来像是在“违反规则”并改变一个对象:
但与常规的突变不同,它不会覆盖过去的状态!
要尝试 Immer:
- 运行
npm install use-immer以将 Immer 添加为依赖项 - 然后将
import { useState } from 'react'替换为import { useImmer } from 'use-immer'
以下是转换为使用 Immer 的上述示例:
请注意事件处理程序变得多么简洁。你可以在单个组件中随意混合使用useState 和 useImmer。Immer 是保持更新处理程序简洁的好方法,尤其是在你的状态中存在嵌套,并且复制对象会导致重复代码的情况下。
回顾
- 将 React 中的所有状态视为不可变的。
- 当你在状态中存储对象时,直接修改它们不会触发渲染,并且会改变先前渲染“快照”中的状态。
- 不要直接修改对象,而是创建它的一个新版本,并通过将状态设置为这个新版本以触发重新渲染。
- 你可以使用
{...obj, something: 'newValue'}对象展开语法来创建对象的副本。 - 展开语法是浅层的:它只复制一层深度。
- 要更新嵌套对象,你需要从你正在更新的位置开始,一路向上创建副本。
- 为了减少重复的复制代码,请使用 Immer。
Try out some challenges
Challenge 1 of 3:Fix incorrect state updates #
This form has a few bugs. Click the button that increases the score a few times. Notice that it does not increase. Then edit the first name, and notice that the score has suddenly “caught up” with your changes. Finally, edit the last name, and notice that the score has disappeared completely.
Your task is to fix all of these bugs. As you fix them, explain why each of them happens.
