State 可以保存任何类型的 JavaScript 值,包括对象。但你不应该直接改变保存在 React state 中的对象。相反,当你想更新一个对象时,你需要创建一个新的对象(或复制一个已存在的对象),然后将 state 设置为使用该副本。
🌐 State can hold any kind of JavaScript value, including objects. But you shouldn’t change objects that you hold in the React state directly. Instead, when you want to update an object, you need to create a new one (or make a copy of an existing one), and then set the state to use that copy.
你将学习到
- 如何正确更新处于 React 状态的对象
- 如何在不改变嵌套对象的情况下更新它
- 什么是不可变性,以及如何不破坏它
- 如何使用 Immer 减少对象复制的重复性
什么是突变?
🌐 What’s a mutation?
你可以在状态中存储任何类型的 JavaScript 值。
🌐 You can store any kind of JavaScript value in state.
const [x, setX] = useState(0);到目前为止,你一直在处理数字、字符串和布尔值。这些类型的 JavaScript 值是“不可变”的,意味着它们不可更改或“只读”。你可以触发重新渲染来_替换_一个值:
🌐 So far you’ve been working with numbers, strings, and booleans. These kinds of JavaScript values are “immutable”, meaning unchangeable or “read-only”. You can trigger a re-render to replace a value:
setX(5);x 状态从 0 变为 5,但 数字 0 本身 并没有改变。在 JavaScript 中,不可能对内置的原始值如数字、字符串和布尔值进行任何修改。
🌐 The x state changed from 0 to 5, but the number 0 itself did not change. It’s not possible to make any changes to the built-in primitive values like numbers, strings, and booleans in JavaScript.
现在考虑一个处于状态的对象:
🌐 Now consider an object in state:
const [position, setPosition] = useState({ x: 0, y: 0 });从技术上讲,修改 对象本身 的内容是可能的。这称为修改:
🌐 Technically, it is possible to change the contents of the object itself. This is called a mutation:
position.x = 5;然而,虽然 React 状态中的对象在技术上是可变的,但你应该把它们当作不可变的——就像数字、布尔值和字符串一样。你应该总是替换它们,而不是修改它们。
🌐 However, although objects in React state are technically mutable, you should treat them as if they were immutable—like numbers, booleans, and strings. Instead of mutating them, you should always replace them.
将状态视为只读
🌐 Treat state as read-only
换句话说,你应该将任何放入状态的 JavaScript 对象视为只读。
🌐 In other words, you should treat any JavaScript object that you put into state as read-only.
此示例在状态中保持一个对象以表示当前指针位置。红点应在你触摸或在预览区域移动光标时移动。但是红点保持在初始位置:
🌐 This example holds an object in state to represent the current pointer position. The red dot is supposed to move when you touch or move the cursor over the preview area. But the dot stays in the initial position:
import { useState } from 'react'; export default function MovingDot() { const [position, setPosition] = useState({ x: 0, y: 0 }); return ( <div onPointerMove={e => { position.x = e.clientX; position.y = e.clientY; }} style={{ position: 'relative', width: '100vw', height: '100vh', }}> <div style={{ position: 'absolute', backgroundColor: 'red', borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} /> </div> ); }
问题在于这段代码。
🌐 The problem is with this bit of code.
onPointerMove={e => {
position.x = e.clientX;
position.y = e.clientY;
}}这段代码修改了分配给 position 的对象,该对象来自之前的渲染。 但如果不使用状态设置函数,React 不会知道对象已更改。因此,React 不会做出任何响应。这就像在你已经吃完饭后试图更改订单一样。虽然在某些情况下修改状态可能有效,但我们不推荐这样做。你应该将渲染中可以访问的状态值视为只读。
🌐 This code modifies the object assigned to position from the previous render. But without using the state setting function, React has no idea that object has changed. So React does not do anything in response. It’s like trying to change the order after you’ve already eaten the meal. While mutating state can work in some cases, we don’t recommend it. You should treat the state value you have access to in a render as read-only.
要在这种情况下实际上触发重新渲染,创建一个新对象并将其传递给状态设置函数:
🌐 To actually trigger a re-render in this case, create a new object and pass it to the state setting function:
onPointerMove={e => {
setPosition({
x: e.clientX,
y: e.clientY
});
}}使用 setPosition,你是在告诉 React:
🌐 With setPosition, you’re telling React:
- 将
position替换为这个新对象 - 并再次渲染这个组件
当你触摸或悬停在预览区域上时,请注意红点现在如何跟随你的指针:
🌐 Notice how the red dot now follows your pointer when you touch or hover over the preview area:
import { useState } from 'react'; export default function MovingDot() { const [position, setPosition] = useState({ x: 0, y: 0 }); return ( <div onPointerMove={e => { setPosition({ x: e.clientX, y: e.clientY }); }} style={{ position: 'relative', width: '100vw', height: '100vh', }}> <div style={{ position: 'absolute', backgroundColor: 'red', borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} /> </div> ); }
深入研究
🌐 Local mutation is fine
像这样的代码是一个问题,因为它修改了状态中的现有对象:
🌐 Code like this is a problem because it modifies an existing object in state:
position.x = e.clientX;
position.y = e.clientY;但像这样的代码是完全没问题的,因为你正在修改一个你刚刚创建的新对象:
🌐 But code like this is absolutely fine because you’re mutating a fresh object you have just created:
const nextPosition = {};
nextPosition.x = e.clientX;
nextPosition.y = e.clientY;
setPosition(nextPosition);事实上,它完全等同于这样写:
🌐 In fact, it is completely equivalent to writing this:
setPosition({
x: e.clientX,
y: e.clientY
});只有在你更改已经处于状态的现有对象时,修改才是一个问题。更改你刚创建的对象是可以的,因为*还没有其他代码引用它。*更改它不会意外影响依赖它的东西。这称为“局部修改”。你甚至可以在渲染时进行局部修改。这非常方便,完全没问题!
🌐 Mutation is only a problem when you change existing objects that are already in state. Mutating an object you’ve just created is okay because no other code references it yet. Changing it isn’t going to accidentally impact something that depends on it. This is called a “local mutation”. You can even do local mutation while rendering. Very convenient and completely okay!
使用展开语法复制对象
🌐 Copying objects with the spread syntax
在前面的例子中,position 对象总是从当前光标位置新建的。但通常情况下,你可能希望将现有数据作为你正在创建的新对象的一部分。例如,你可能只想更新表单中的某一个字段,但保留其他所有字段的先前值。
🌐 In the previous example, the position object is always created fresh from the current cursor position. But often, you will want to include existing data as a part of the new object you’re creating. For example, you may want to update only one field in a form, but keep the previous values for all other fields.
这些输入字段无法使用,因为 onChange 处理程序会更改状态:
🌐 These input fields don’t work because the onChange handlers mutate the state:
import { useState } from 'react'; export default function Form() { const [person, setPerson] = useState({ firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com' }); function handleFirstNameChange(e) { person.firstName = e.target.value; } function handleLastNameChange(e) { person.lastName = e.target.value; } function handleEmailChange(e) { person.email = e.target.value; } return ( <> <label> First name: <input value={person.firstName} onChange={handleFirstNameChange} /> </label> <label> Last name: <input value={person.lastName} onChange={handleLastNameChange} /> </label> <label> Email: <input value={person.email} onChange={handleEmailChange} /> </label> <p> {person.firstName}{' '} {person.lastName}{' '} ({person.email}) </p> </> ); }
例如,这一行改变了过去渲染的状态:
🌐 For example, this line mutates the state from a past render:
person.firstName = e.target.value;获得所需行为的可靠方法是创建一个新对象并将其传递给 setPerson。但是在这里,你还想 将现有数据复制到其中,因为只有其中一个字段发生了变化:
🌐 The reliable way to get the behavior you’re looking for is to create a new object and pass it to setPerson. But here, you want to also copy the existing data into it because only one of the fields has changed:
setPerson({
firstName: e.target.value, // New first name from the input
lastName: person.lastName,
email: person.email
});你可以使用 ... 对象展开 语法,这样你就不需要单独复制每个属性。
🌐 You can use the ... object spread syntax so that you don’t need to copy every property separately.
setPerson({
...person, // Copy the old fields
firstName: e.target.value // But override this one
});现在表单生效了!
🌐 Now the form works!
注意你没有为每个输入字段声明一个单独的状态变量。对于大型表单,将所有数据组织在一个对象中是非常方便的——只要你正确更新它!
🌐 Notice how you didn’t declare a separate state variable for each input field. For large forms, keeping all data grouped in an object is very convenient—as long as you update it correctly!
import { useState } from 'react'; export default function Form() { const [person, setPerson] = useState({ firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com' }); function handleFirstNameChange(e) { setPerson({ ...person, firstName: e.target.value }); } function handleLastNameChange(e) { setPerson({ ...person, lastName: e.target.value }); } function handleEmailChange(e) { setPerson({ ...person, email: e.target.value }); } return ( <> <label> First name: <input value={person.firstName} onChange={handleFirstNameChange} /> </label> <label> Last name: <input value={person.lastName} onChange={handleLastNameChange} /> </label> <label> Email: <input value={person.email} onChange={handleEmailChange} /> </label> <p> {person.firstName}{' '} {person.lastName}{' '} ({person.email}) </p> </> ); }
请注意,... 扩展语法是“浅拷贝”的——它只会拷贝一层。这使得它很快,但这也意味着如果你想更新嵌套属性,你将不得不使用它多次。
🌐 Note that the ... spread syntax is “shallow”—it only copies things one level deep. This makes it fast, but it also means that if you want to update a nested property, you’ll have to use it more than once.
深入研究
🌐 Using a single event handler for multiple fields
你还可以在对象定义中使用 [ 和 ] 大括号来指定具有动态名称的属性。下面是相同的示例,但使用单个事件处理程序而不是三个不同的处理程序:
🌐 You can also use the [ and ] braces inside your object definition to specify a property with a dynamic name. Here is the same example, but with a single event handler instead of three different ones:
import { useState } from 'react'; export default function Form() { const [person, setPerson] = useState({ firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com' }); function handleChange(e) { setPerson({ ...person, [e.target.name]: e.target.value }); } return ( <> <label> First name: <input name="firstName" value={person.firstName} onChange={handleChange} /> </label> <label> Last name: <input name="lastName" value={person.lastName} onChange={handleChange} /> </label> <label> Email: <input name="email" value={person.email} onChange={handleChange} /> </label> <p> {person.firstName}{' '} {person.lastName}{' '} ({person.email}) </p> </> ); }
这里,e.target.name 指的是赋给 <input> DOM 元素的 name 属性。
🌐 Here, e.target.name refers to the name property given to the <input> DOM element.
更新嵌套对象
🌐 Updating a nested object
考虑这样的嵌套对象结构:
🌐 Consider a nested object structure like this:
const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});如果你想更新 person.artwork.city,很清楚如何使用修改来做到这一点:
🌐 If you wanted to update person.artwork.city, it’s clear how to do it with mutation:
person.artwork.city = 'New Delhi';但是在 React 中,你要把 state 当作不可变的!为了改变 city,你首先需要生成新的 artwork 对象(用之前对象的数据预先填充),然后生成指向新的 artwork 的 person 对象:
🌐 But in React, you treat state as immutable! In order to change city, you would first need to produce the new artwork object (pre-populated with data from the previous one), and then produce the new person object which points at the new artwork:
const nextArtwork = { ...person.artwork, city: 'New Delhi' };
const nextPerson = { ...person, artwork: nextArtwork };
setPerson(nextPerson);或者,写成单个函数调用:
🌐 Or, written as a single function call:
setPerson({
...person, // Copy other fields
artwork: { // but replace the artwork
...person.artwork, // with the same one
city: 'New Delhi' // but in New Delhi!
}
});这有点罗嗦,但在很多情况下都可以正常工作:
🌐 This gets a bit wordy, but it works fine for many cases:
import { useState } from 'react'; export default function Form() { const [person, setPerson] = useState({ name: 'Niki de Saint Phalle', artwork: { title: 'Blue Nana', city: 'Hamburg', image: 'https://i.imgur.com/Sd1AgUOm.jpg', } }); function handleNameChange(e) { setPerson({ ...person, name: e.target.value }); } function handleTitleChange(e) { setPerson({ ...person, artwork: { ...person.artwork, title: e.target.value } }); } function handleCityChange(e) { setPerson({ ...person, artwork: { ...person.artwork, city: e.target.value } }); } function handleImageChange(e) { setPerson({ ...person, artwork: { ...person.artwork, image: e.target.value } }); } return ( <> <label> Name: <input value={person.name} onChange={handleNameChange} /> </label> <label> Title: <input value={person.artwork.title} onChange={handleTitleChange} /> </label> <label> City: <input value={person.artwork.city} onChange={handleCityChange} /> </label> <label> Image: <input value={person.artwork.image} onChange={handleImageChange} /> </label> <p> <i>{person.artwork.title}</i> {' by '} {person.name} <br /> (located in {person.artwork.city}) </p> <img src={person.artwork.image} alt={person.artwork.title} /> </> ); }
深入研究
🌐 Objects are not really nested
这样的对象在代码中显示为“嵌套”:
🌐 An object like this appears “nested” in code:
let obj = {
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
};然而,‘嵌套’并不是理解对象行为的准确方式。当代码执行时,并不存在所谓的‘嵌套’对象。你实际上是在看两个不同的对象:
🌐 However, “nesting” is an inaccurate way to think about how objects behave. When the code executes, there is no such thing as a “nested” object. You are really looking at two different objects:
let obj1 = {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
};
let obj2 = {
name: 'Niki de Saint Phalle',
artwork: obj1
};obj1 对象不在 obj2 “内部”。例如,obj3 也可以“指向” obj1:
🌐 The obj1 object is not “inside” obj2. For example, obj3 could “point” at obj1 too:
let obj1 = {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
};
let obj2 = {
name: 'Niki de Saint Phalle',
artwork: obj1
};
let obj3 = {
name: 'Copycat',
artwork: obj1
};如果你修改 obj3.artwork.city,它会影响 obj2.artwork.city 和 obj1.city。这是因为 obj3.artwork、obj2.artwork 和 obj1 是同一个对象。当你把对象看作“嵌套”的时候,这一点很难看清。实际上,它们是相互“指向”的独立对象,通过属性连接。
🌐 If you were to mutate obj3.artwork.city, it would affect both obj2.artwork.city and obj1.city. This is because obj3.artwork, obj2.artwork, and obj1 are the same object. This is difficult to see when you think of objects as “nested”. Instead, they are separate objects “pointing” at each other with properties.
使用 Immer 编写简洁的更新逻辑
🌐 Write concise update logic with Immer
如果你的状态结构非常嵌套,你可能需要考虑扁平化它。但是,如果你不想改变状态结构,你可能会更喜欢嵌套展开的快捷方式。Immer 是一个流行的库,它允许你使用方便但会变更状态的语法来编写代码,并为你处理生成副本。有了 Immer,你编写的代码看起来就像是在“破坏规则”并修改一个对象:
🌐 If your state is deeply nested, you might want to consider flattening it. But, if you don’t want to change your state structure, you might prefer a shortcut to nested spreads. Immer is a popular library that lets you write using the convenient but mutating syntax and takes care of producing the copies for you. With Immer, the code you write looks like you are “breaking the rules” and mutating an object:
updatePerson(draft => {
draft.artwork.city = 'Lagos';
});但与常规突变不同的是,它不会覆盖过去的状态!
🌐 But unlike a regular mutation, it doesn’t overwrite the past state!
深入研究
🌐 How does Immer work?
Immer 提供的 draft 是一种特殊类型的对象,称为 Proxy,它可以“记录”你对它所做的操作。这就是为什么你可以随意修改它的原因!在底层,Immer 会找出 draft 的哪些部分被更改,并生成一个包含你编辑内容的全新对象。
🌐 The draft provided by Immer is a special type of object, called a Proxy, that “records” what you do with it. This is why you can mutate it freely as much as you like! Under the hood, Immer figures out which parts of the draft have been changed, and produces a completely new object that contains your edits.
尝试 Immer:
🌐 To try Immer:
- 运行
npm install use-immer来添加 Immer 作为依赖 - 然后将
import { useState } from 'react'替换为import { useImmer } from 'use-immer'
这是上面的示例转换为 Immer:
🌐 Here is the above example converted to 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": {} }
注意事件处理程序变得多么简洁。你可以在单个组件中任意混合使用 useState 和 useImmer。Immer 是保持更新处理程序简洁的好方法,特别是当你的状态中有嵌套时,而复制对象会导致重复的代码。
🌐 Notice how much more concise the event handlers have become. You can mix and match useState and useImmer in a single component as much as you like. Immer is a great way to keep the update handlers concise, especially if there’s nesting in your state, and copying objects leads to repetitive code.
深入研究
🌐 Why is mutating state not recommended in React?
有几个原因:
🌐 There are a few reasons:
- 调试: 如果你使用
console.log并且不改变状态,你过去的日志就不会被最近的状态变化覆盖。因此,你可以清楚地看到状态在渲染之间是如何变化的。 - 优化: 常见的 React 优化策略 依赖于在前一个 props 或 state 与下一个相同时跳过工作。如果你从不改变 state,那么检查是否有任何变化将非常快。如果
prevObj === obj,你可以确定它内部的内容不可能发生变化。 - 新功能: 我们正在构建的新 React 功能依赖于将状态视为快照。如果你在修改状态的历史版本,这可能会阻止你使用这些新功能。
- 需求变更: 一些应用功能,比如实现撤销/重做、显示更改历史,或者让用户将表单重置为早期数值,在没有任何变更的情况下更容易实现。这是因为你可以在内存中保留状态的过去副本,并在合适的时候重用它们。如果你一开始采用可变的方式,这类功能后来可能很难添加。
- 更简单的实现: 因为 React 不依赖于变更,它不需要对你的对象做任何特殊处理。它不需要劫持它们的属性,总是将它们封装到 Proxy 中,或者像许多“响应式”解决方案那样在初始化时做其他工作。这也是为什么 React 允许你将任何对象放入 state——无论大小——而不会带来额外的性能或正确性问题。
在实际操作中,你通常可以在 React 中“侥幸”修改状态,但我们强烈建议你不要这样做,这样你才能使用以这种方法开发的新 React 功能。未来的贡献者,甚至你未来的自己都会感谢你!
🌐 In practice, you can often “get away” with mutating state in React, but we strongly advise you not to do that so that you can use new React features developed with this approach in mind. Future contributors and perhaps even your future self will thank you!
回顾
- 将 React 中的所有状态视为不可变的。
- 当你在 state 中存储对象时,修改它们不会触发渲染,并且会改变之前渲染“快照”中的 state。
- 不要修改对象本身,而是创建它的新版本,然后通过将状态设置为该版本来触发重新渲染。
- 你可以使用
{...obj, something: 'newValue'}对象展开语法来创建对象的副本。 - 展开语法是浅拷贝:它只复制一级内容。
- 要更新嵌套对象,你需要从要更新的位置一直向上创建副本。
- 要减少重复复制代码,请使用 Immer。
挑战 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.
import { useState } from 'react'; export default function Scoreboard() { const [player, setPlayer] = useState({ firstName: 'Ranjani', lastName: 'Shettar', score: 10, }); function handlePlusClick() { player.score++; } function handleFirstNameChange(e) { setPlayer({ ...player, firstName: e.target.value, }); } function handleLastNameChange(e) { setPlayer({ lastName: e.target.value }); } return ( <> <label> Score: <b>{player.score}</b> {' '} <button onClick={handlePlusClick}> +1 </button> </label> <label> First name: <input value={player.firstName} onChange={handleFirstNameChange} /> </label> <label> Last name: <input value={player.lastName} onChange={handleLastNameChange} /> </label> </> ); }