状态可以保存任何类型的 JavaScript 值,包括对象。但是你不应该直接改变你在 React 状态下持有的对象。而是,当你想更新一个对象时,你需要创建一个新对象(或复制一个现有对象),然后设置状态以使用该副本。
¥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 状态的对象
¥How to correctly update an object in React state
-
如何在不改变嵌套对象的情况下更新它
¥How to update a nested object without mutating it
-
什么是不可变性,以及如何不破坏它
¥What immutability is, and how not to break it
-
如何使用 Immer 减少对象复制的重复性
¥How to make object copying less repetitive with Immer
什么是突变?
¥What’s a mutation?
你可以在状态中存储任何类型的 JavaScript 值。
¥You can store any kind of JavaScript value in state.
const [x, setX] = useState(0);
到目前为止,你一直在处理数字、字符串和布尔值。这些类型的 JavaScript 值是 “不可变的”,表示不可更改或 “read-only”。你可以触发重新渲染以替换值:
¥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
¥Replace
position
with this new object -
并再次渲染这个组件
¥And render this component again
当你触摸或悬停在预览区域上时,请注意红点现在如何跟随你的指针:
¥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 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 中,你将状态视为不可变的!为了更改 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
是一种特殊类型的对象,称为 代理,即 “记录” 你用它做什么。这就是为什么你可以随心所欲地改变它!在引擎盖下,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 添加为依赖¥Run
npm install use-immer
to add Immer as a dependency -
然后用
import { useImmer } from 'use-immer'
替换import { useState } from 'react'
¥Then replace
import { useState } from 'react'
withimport { 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
并且不改变状态,你过去的日志将不会被最近的状态更改所破坏。所以你可以清楚地看到状态在渲染之间是如何变化的。¥Debugging: If you use
console.log
and don’t mutate state, your past logs won’t get clobbered by the more recent state changes. So you can clearly see how state has changed between renders. -
优化如果前一个属性或状态与下一个相同,则通用 React 优化策略 依赖于跳过工作。如果你从不改变状态,那么检查是否有任何变化是非常快的。如果是
prevObj === obj
,你可以确定它内部没有任何变化。¥Optimizations: Common React optimization strategies rely on skipping work if previous props or state are the same as the next ones. If you never mutate state, it is very fast to check whether there were any changes. If
prevObj === obj
, you can be sure that nothing could have changed inside of it. -
新功能:我们正在构建的新 React 功能依赖于状态为 像快照一样对待。 如果你正在改变过去版本的状态,那可能会阻止你使用新功能。
¥New Features: The new React features we’re building rely on state being treated like a snapshot. If you’re mutating past versions of state, that may prevent you from using the new features.
-
要求变更:某些应用功能,如实现撤消/重做、显示更改历史或让用户将表单重置为较早的值,在没有任何变化时更容易实现。这是因为你可以在内存中保留过去的状态副本,并在适当的时候重用它们。如果你从可变方法开始,以后可能很难添加此类功能。
¥Requirement Changes: Some application features, like implementing Undo/Redo, showing a history of changes, or letting the user reset a form to earlier values, are easier to do when nothing is mutated. This is because you can keep past copies of state in memory, and reuse them when appropriate. If you start with a mutative approach, features like this can be difficult to add later on.
-
更简单的实现:因为 React 不依赖于修改,所以它不需要对你的对象做任何特殊的事情。它不需要劫持它们的属性,总是将它们封装到代理中,或者像许多 “反应式” 解决方案那样在初始化时做其他工作。这也是 React 允许你将任何对象放入状态的原因 - 无论对象有多大 - 没有额外的性能或正确性缺陷。
¥Simpler Implementation: Because React does not rely on mutation, it does not need to do anything special with your objects. It does not need to hijack their properties, always wrap them into Proxies, or do other work at initialization as many “reactive” solutions do. This is also why React lets you put any object into state—no matter how large—without additional performance or correctness pitfalls.
在实践中,你通常可以在 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 中的所有状态视为不可变的。
¥Treat all state in React as immutable.
-
当你将对象存储在状态中时,改变它们不会触发渲染,并且会更改先前渲染 “快照” 中的状态。
¥When you store objects in state, mutating them will not trigger renders and will change the state in previous render “snapshots”.
-
与其改变对象,不如创建它的新版本,并通过为其设置状态来触发重新渲染。
¥Instead of mutating an object, create a new version of it, and trigger a re-render by setting state to it.
-
你可以使用
{...obj, something: 'newValue'}
对象展开语法来创建对象的副本。¥You can use the
{...obj, something: 'newValue'}
object spread syntax to create copies of objects. -
展开语法很浅:它只复制一层深。
¥Spread syntax is shallow: it only copies one level deep.
-
要更新嵌套对象,你需要从要更新的位置一直向上创建副本。
¥To update a nested object, you need to create copies all the way up from the place you’re updating.
-
要减少重复复制代码,请使用 Immer。
¥To reduce repetitive copying code, use Immer.
挑战 1 / 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> </> ); }