State可以是Javascript的任意数据类型,包含object数据类型。但是,不能直接修改React state的object值。如果想更改一个object,需要创建一个新的object(或是复制之前的object生成新的object),然后使用新的object给state赋值。
不可变的数据
可以在State存储任意类型的JavaScript值:
const [x, setX] = useState(0);
number、string和boolean类型的数值是不可变的,或是只读的数据。替换一个值的时候触发一个重新渲染:
setX(5);
state X的值从0变到5,数值0本身是不可改变的。JavaScript不可能对基本数据类型的值(如数number、string和boolean)进行任何更改。
现在我们看下state是object类型时:
const [position, setPosition] = useState({ x: 0, y: 0 });
尽管,然而,尽管React state中的object在技术上是可变的,但是你要把它看做是不可改变的—就像number、string和boolean一样。不是修改object, 而是用新的object替换object。
把state看做仅是可读的
把state的Javascript object看作是仅仅可读的,不能修改。
下面这个例子,在state object存储的是光标位置。当在预览区域上触摸或移动光标时,红点应该会移动。但是红点一直在原始的位置:
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>
);
}
问题在于这段代码。
onPointerMove={e => {
position.x = e.clientX;
position.y = e.clientY;
}}
这段代码直接修改了position对象。没有使用state的set函数,React不知道object改变了,所以React没有做任何回应。把state值看作是仅仅可读的,不能直接修改state的数据。 要想触发一个重新渲染,创建一个新的object并且把它传给设置state函数:
onPointerMove={e => {
setPosition({
x: e.clientX,
y: e.clientY
});
}}
使用setPosition,你告诉React:
- 用新的object替换position
- 重新渲染这个组件
现在当在预览区域上触摸或移动光标时,红点会跟着移动。
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>
);
}
使用扩展语法复制object
在前面的例子中,总是根据当前的光标位置,创建一个新的position对象。有时候需要把现有的对象的属性包含在一个对象中,作为另一个对象的一部分。例如,你想更新表单中一个字段,其他的字段都不改变。
下面的例子输入不会改变,因为onChange事件处理函数直接修改了person对象:
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>
</>
);
}
比如这一行直接修改了person对象:
person.firstName = e.target.value;
可靠的方法是创建一个新的对象并且把它传递给setPerson。在这里,只有一个字段改变了,你需要把其他的没有改变的字段复制到新的对象里:
setPerson({
firstName: e.target.value, // New first name from the input
lastName: person.lastName,
email: person.email
});
你可以用…对象展开语法,这样就不需要单独复制每个属性。
setPerson({
...person, // Copy the old fields
firstName: e.target.value // But override this one
});
现在表单能够正常运行了。
没有为每个输入字段声明单独的状态变量。对于大型表单,将所有数据分组在一个对象中非常方便 —— 只要正确地更新它!
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>
</>
);
}
...展开语法是“浅的 ”— 它只复制一层。这使得它更快,但这也意味着如果想要更新一个嵌套属性,不得不多次使用它。
更新一个嵌套对象
嵌套对象如下:
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,首选先创建一个新的artwork对象,然后创建一个新的person对象指向新的artwork对象:
const nextArtwork = { ...person.artwork, city: 'New Delhi' };
const nextPerson = { ...person, artwork: nextArtwork };
setPerson(nextPerson);
或是写成单个函数调用:
setPerson({
...person, // Copy other fields
artwork: { // but replace the artwork
...person.artwork, // with the same one
city: 'New Delhi' // but in New Delhi!
}
});
实现如下:
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>
</>
);
}
用Immer简化逻辑
Immer是一个比较流行的库,使你能够直接修改数据,并且负责给创建一份副本。使用Immer能够直接修改一个object:
updatePerson(draft => {
draft.artwork.city = 'Lagos';
});
试一下Immer:
- 运行npm install use-immer命令将Immer添加为依赖
- 然后将'react'中的import {useState}替换为'use-immer'中的import {useImmer}
下面是上面的例子转换成Immer格式:
import { useImmer } from 'use-immer';
export default function Form() {
const [person, updatePerson] = useImmer({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});
function handleNameChange(e) {
updatePerson(draft => {
draft.name = e.target.value;
});
}
function handleTitleChange(e) {
updatePerson(draft => {
draft.artwork.title = e.target.value;
});
}
function handleCityChange(e) {
updatePerson(draft => {
draft.artwork.city = e.target.value;
});
}
function handleImageChange(e) {
updatePerson(draft => {
draft.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>
</>
);
}
事件处理程序变的简洁了。可以在单个组件中随意混合使用useState和useImmer。Immer是一个让事件处理函数变的简洁的好的方法,特别是在state中有嵌套复制对象会有重复的代码的时候。