React?18?如何更新?state?中的對象
更新 state 中的對象
state 中可以保存任意類型的 JavaScript 值,包括對象。但是,不應(yīng)該直接修改存放在 React state 中的對象。相反,當(dāng)想要更新一個對象時,需要創(chuàng)建一個新的對象(或者將其拷貝一份),然后將 state 更新為此對象。
什么是 mutation?
可以在 state 中存放任意類型的 JavaScript 值。
const [x, setX] = useState(0);
在 state 中存放數(shù)字、字符串和布爾值,這些類型的值在 JavaScript 中是不可變(immutable)的,這意味著它們不能被改變或是只讀的??梢酝ㄟ^替換它們的值以觸發(fā)一次重新渲染。
setX(5);
state x 從 0 變?yōu)?5 ,但是數(shù)字 0 本身并沒有發(fā)生改變。在 JavaScript 中,無法對內(nèi)置的原始值,如數(shù)字、字符串和布爾值,進(jìn)行任何更改。
現(xiàn)在考慮 state 中存放對象的情況:
const [position, setPosition] = useState({ x: 0, y: 0 });從技術(shù)上來講,可以改變對象自身的內(nèi)容。當(dāng)這樣做時,就制造了一個 mutation:
position.x = 5;
然而,雖然嚴(yán)格來說 React state 中存放的對象是可變的,但應(yīng)該像處理數(shù)字、布爾值、字符串一樣將它們視為不可變的。因此應(yīng)該替換它們的值,而不是對它們進(jìn)行修改。
將 state 視為只讀的
換句話說,應(yīng)該 把所有存放在 state 中的 JavaScript 對象都視為只讀的。
在下面的例子中,用一個存放在 state 中的對象來表示指針當(dāng)前的位置。當(dāng)在預(yù)覽區(qū)觸摸或移動光標(biāo)時,紅色的點(diǎn)本應(yīng)移動。但是實(shí)際上紅點(diǎn)仍停留在原處:
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 的對象。但是因?yàn)椴]有使用 state 的設(shè)置函數(shù),React 并不知道對象已更改。所以 React 沒有做出任何響應(yīng)。雖然在一些情況下,直接修改 state 可能是有效的,但并不推薦這么做。應(yīng)該把在渲染過程中可以訪問到的 state 視為只讀的。
在這種情況下,為了真正地 觸發(fā)一次重新渲染,需要創(chuàng)建一個新對象并把它傳遞給 state 的設(shè)置函數(shù):
onPointerMove={e => {
setPosition({
x: e.clientX,
y: e.clientY
});
}}通過使用 setPosition ,在告訴 React:
- 使用這個新的對象替換
position的值 - 然后再次渲染這個組件
現(xiàn)在可以看到,當(dāng)在預(yù)覽區(qū)觸摸或移動光標(biāo)時,紅點(diǎn)會跟隨著指針移動:
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>
);
}使用展開語法復(fù)制對象
在之前的例子中,始終會根據(jù)當(dāng)前指針的位置創(chuàng)建出一個新的 position 對象。但是通常,會希望把 現(xiàn)有 數(shù)據(jù)作為所創(chuàng)建的新對象的一部分。例如,可能只想要更新表單中的一個字段,其他的字段仍然使用之前的值。
下面的代碼中,輸入框并不會正常運(yùn)行,因?yàn)?onChange 直接修改了 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>
</>
);
}例如,下面這行代碼修改了上一次渲染中的 state:
person.firstName = e.target.value;
想要實(shí)現(xiàn)需求,最可靠的辦法就是創(chuàng)建一個新的對象并將它傳遞給 setPerson 。但是在這里,還需要 把當(dāng)前的數(shù)據(jù)復(fù)制到新對象中,因?yàn)橹桓淖兞似渲幸粋€字段:
setPerson({
firstName: e.target.value, // 從 input 中獲取新的 first name
lastName: person.lastName,
email: person.email
});可以使用 ... 對象展開 語法,這樣就不需要單獨(dú)復(fù)制每個屬性。
setPerson({
...person, // 復(fù)制上一個 person 中的所有字段
firstName: e.target.value // 但是覆蓋 firstName 字段
});現(xiàn)在表單可以正常運(yùn)行了!
可以看到,并沒有為每個輸入框單獨(dú)聲明一個 state。對于大型表單,將所有數(shù)據(jù)都存放在同一個對象中是非常方便的——前提是能夠正確地更新它!
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>
</>
);
}請注意 ... 展開語法本質(zhì)是“淺拷貝”——它只會復(fù)制一層。這使得它的執(zhí)行速度很快,但是也意味著當(dāng)想要更新一個嵌套屬性時,必須得多次使用展開語法。
更新一個嵌套對象
考慮下面這種結(jié)構(gòu)的嵌套對象:
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 的值,用 mutation 來實(shí)現(xiàn)的方法非常容易理解:
person.artwork.city = 'New Delhi';
但是在 React 中,需要將 state 視為不可變的!為了修改 city 的值,首先需要創(chuàng)建一個新的 artwork 對象(其中預(yù)先填充了上一個 artwork 對象中的數(shù)據(jù)),然后創(chuàng)建一個新的 person 對象,并使得其中的 artwork 屬性指向新創(chuàng)建的 artwork 對象:
const nextArtwork = { ...person.artwork, city: 'New Delhi' };
const nextPerson = { ...person, artwork: nextArtwork };
setPerson(nextPerson);或者,寫成一個函數(shù)調(diào)用:
setPerson({
...person, // 復(fù)制其它字段的數(shù)據(jù)
artwork: { // 替換 artwork 字段
...person.artwork, // 復(fù)制之前 person.artwork 中的數(shù)據(jù)
city: 'New Delhi' // 但是將 city 的值替換為 New Delhi!
}
});這雖然看起來有點(diǎn)冗長,但對于很多情況都能有效地解決問題:
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}
/>
</>
);
}使用 Immer 編寫簡潔的更新邏輯
如果 state 有多層的嵌套,或許應(yīng)該考慮 將其扁平化。但是,如果不想改變 state 的數(shù)據(jù)結(jié)構(gòu),可以使用 Immer 來實(shí)現(xiàn)嵌套展開的效果。Immer 是一個非常流行的庫,它可以讓你使用簡便但可以直接修改的語法編寫代碼,并會幫你處理好復(fù)制的過程。通過使用 Immer,寫出的代碼看起來就像是“打破了規(guī)則”而直接修改了對象:
updatePerson(draft => {
draft.artwork.city = 'Lagos';
});但是不同于一般的 mutation,它并不會覆蓋之前的 state!
嘗試使用 Immer:
- 運(yùn)行
npm install use-immer添加 Immer 依賴 - 用
import { useImmer } from 'use-immer'替換掉import { useState } from 'react'
下面我們把上面的例子用 Immer 實(shí)現(xiàn)一下:
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>
<img
src={person.artwork.image}
alt={person.artwork.title}
/>
</>
);
}package.json:
{
"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": {}
}可以看到,事件處理函數(shù)變得更簡潔了。可以隨意在一個組件中同時使用 useState 和 useImmer。如果想要寫出更簡潔的更新處理函數(shù),Immer 會是一個不錯的選擇,尤其是當(dāng) state 中有嵌套,并且復(fù)制對象會帶來重復(fù)的代碼時。
摘要
- 將 React 中所有的 state 都視為不可直接修改的。
- 當(dāng)在 state 中存放對象時,直接修改對象并不會觸發(fā)重渲染,并會改變前一次渲染“快照”中 state 的值。
- 不要直接修改一個對象,而要為它創(chuàng)建一個 新 版本,并通過把 state 設(shè)置成這個新版本來觸發(fā)重新渲染。
- 可以使用這樣的 {...obj, something: 'newValue'} 對象展開語法來創(chuàng)建對象的拷貝。
- 對象的展開語法是淺層的:它的復(fù)制深度只有一層。
- 想要更新嵌套對象,需要從更新的位置開始自底向上為每一層都創(chuàng)建新的拷貝。
- 想要減少重復(fù)的拷貝代碼,可以使用 Immer。
到此這篇關(guān)于React 18 更新 state 中的對象的文章就介紹到這了,更多相關(guān)React更新 state 對象內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React替換傳統(tǒng)拷貝方法的Immutable使用
Immutable.js出自Facebook,是最流行的不可變數(shù)據(jù)結(jié)構(gòu)的實(shí)現(xiàn)之一。它實(shí)現(xiàn)了完全的持久化數(shù)據(jù)結(jié)構(gòu),使用結(jié)構(gòu)共享。所有的更新操作都會返回新的值,但是在內(nèi)部結(jié)構(gòu)是共享的,來減少內(nèi)存占用2023-02-02
React-View-UI組件庫封裝Loading加載中源碼
這篇文章主要介紹了React-View-UI組件庫封裝Loading加載樣式,主要包括組件介紹,組件源碼及組件測試源碼,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-06-06
Can't?perform?a?React?state?update?on?an?unmoun
這篇文章主要為大家介紹了Can't?perform?a?React?state?update?on?an?unmounted?component報(bào)錯解決方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
react實(shí)現(xiàn)數(shù)據(jù)監(jiān)聽方式
這篇文章主要介紹了react實(shí)現(xiàn)數(shù)據(jù)監(jiān)聽方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08

