React?18?如何更新?state?中的對(duì)象
更新 state 中的對(duì)象
state 中可以保存任意類型的 JavaScript 值,包括對(duì)象。但是,不應(yīng)該直接修改存放在 React state 中的對(duì)象。相反,當(dāng)想要更新一個(gè)對(duì)象時(shí),需要?jiǎng)?chuàng)建一個(gè)新的對(duì)象(或者將其拷貝一份),然后將 state 更新為此對(duì)象。
什么是 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 中,無法對(duì)內(nèi)置的原始值,如數(shù)字、字符串和布爾值,進(jìn)行任何更改。
現(xiàn)在考慮 state 中存放對(duì)象的情況:
const [position, setPosition] = useState({ x: 0, y: 0 });
從技術(shù)上來講,可以改變對(duì)象自身的內(nèi)容。當(dāng)這樣做時(shí),就制造了一個(gè) mutation:
position.x = 5;
然而,雖然嚴(yán)格來說 React state 中存放的對(duì)象是可變的,但應(yīng)該像處理數(shù)字、布爾值、字符串一樣將它們視為不可變的。因此應(yīng)該替換它們的值,而不是對(duì)它們進(jìn)行修改。
將 state 視為只讀的
換句話說,應(yīng)該 把所有存放在 state 中的 JavaScript 對(duì)象都視為只讀的。
在下面的例子中,用一個(gè)存放在 state 中的對(duì)象來表示指針當(dāng)前的位置。當(dāng)在預(yù)覽區(qū)觸摸或移動(dòng)光標(biāo)時(shí),紅色的點(diǎn)本應(yīng)移動(dò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
的對(duì)象。但是因?yàn)椴]有使用 state 的設(shè)置函數(shù),React 并不知道對(duì)象已更改。所以 React 沒有做出任何響應(yīng)。雖然在一些情況下,直接修改 state 可能是有效的,但并不推薦這么做。應(yīng)該把在渲染過程中可以訪問到的 state 視為只讀的。
在這種情況下,為了真正地 觸發(fā)一次重新渲染,需要?jiǎng)?chuàng)建一個(gè)新對(duì)象并把它傳遞給 state 的設(shè)置函數(shù):
onPointerMove={e => { setPosition({ x: e.clientX, y: e.clientY }); }}
通過使用 setPosition
,在告訴 React:
- 使用這個(gè)新的對(duì)象替換
position
的值 - 然后再次渲染這個(gè)組件
現(xiàn)在可以看到,當(dāng)在預(yù)覽區(qū)觸摸或移動(dòng)光標(biāo)時(shí),紅點(diǎn)會(huì)跟隨著指針移動(dòng):
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ù)制對(duì)象
在之前的例子中,始終會(huì)根據(jù)當(dāng)前指針的位置創(chuàng)建出一個(gè)新的 position
對(duì)象。但是通常,會(huì)希望把 現(xiàn)有 數(shù)據(jù)作為所創(chuàng)建的新對(duì)象的一部分。例如,可能只想要更新表單中的一個(gè)字段,其他的字段仍然使用之前的值。
下面的代碼中,輸入框并不會(huì)正常運(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)建一個(gè)新的對(duì)象并將它傳遞給 setPerson
。但是在這里,還需要 把當(dāng)前的數(shù)據(jù)復(fù)制到新對(duì)象中,因?yàn)橹桓淖兞似渲幸粋€(gè)字段:
setPerson({ firstName: e.target.value, // 從 input 中獲取新的 first name lastName: person.lastName, email: person.email });
可以使用 ...
對(duì)象展開 語法,這樣就不需要單獨(dú)復(fù)制每個(gè)屬性。
setPerson({ ...person, // 復(fù)制上一個(gè) person 中的所有字段 firstName: e.target.value // 但是覆蓋 firstName 字段 });
現(xiàn)在表單可以正常運(yùn)行了!
可以看到,并沒有為每個(gè)輸入框單獨(dú)聲明一個(gè) state。對(duì)于大型表單,將所有數(shù)據(jù)都存放在同一個(gè)對(duì)象中是非常方便的——前提是能夠正確地更新它!
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> </> ); }
請(qǐng)注意 ...
展開語法本質(zhì)是“淺拷貝”——它只會(huì)復(fù)制一層。這使得它的執(zhí)行速度很快,但是也意味著當(dāng)想要更新一個(gè)嵌套屬性時(shí),必須得多次使用展開語法。
更新一個(gè)嵌套對(duì)象
考慮下面這種結(jié)構(gòu)的嵌套對(duì)象:
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
的值,首先需要?jiǎng)?chuàng)建一個(gè)新的 artwork
對(duì)象(其中預(yù)先填充了上一個(gè) artwork
對(duì)象中的數(shù)據(jù)),然后創(chuàng)建一個(gè)新的 person
對(duì)象,并使得其中的 artwork
屬性指向新創(chuàng)建的 artwork
對(duì)象:
const nextArtwork = { ...person.artwork, city: 'New Delhi' }; const nextPerson = { ...person, artwork: nextArtwork }; setPerson(nextPerson);
或者,寫成一個(gè)函數(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)冗長(zhǎng),但對(duì)于很多情況都能有效地解決問題:
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 編寫簡(jiǎn)潔的更新邏輯
如果 state 有多層的嵌套,或許應(yīng)該考慮 將其扁平化。但是,如果不想改變 state 的數(shù)據(jù)結(jié)構(gòu),可以使用 Immer 來實(shí)現(xiàn)嵌套展開的效果。Immer 是一個(gè)非常流行的庫,它可以讓你使用簡(jiǎn)便但可以直接修改的語法編寫代碼,并會(huì)幫你處理好復(fù)制的過程。通過使用 Immer,寫出的代碼看起來就像是“打破了規(guī)則”而直接修改了對(duì)象:
updatePerson(draft => { draft.artwork.city = 'Lagos'; });
但是不同于一般的 mutation,它并不會(huì)覆蓋之前的 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ù)變得更簡(jiǎn)潔了??梢噪S意在一個(gè)組件中同時(shí)使用 useState
和 useImmer
。如果想要寫出更簡(jiǎn)潔的更新處理函數(shù),Immer 會(huì)是一個(gè)不錯(cuò)的選擇,尤其是當(dāng) state 中有嵌套,并且復(fù)制對(duì)象會(huì)帶來重復(fù)的代碼時(shí)。
摘要
- 將 React 中所有的 state 都視為不可直接修改的。
- 當(dāng)在 state 中存放對(duì)象時(shí),直接修改對(duì)象并不會(huì)觸發(fā)重渲染,并會(huì)改變前一次渲染“快照”中 state 的值。
- 不要直接修改一個(gè)對(duì)象,而要為它創(chuàng)建一個(gè) 新 版本,并通過把 state 設(shè)置成這個(gè)新版本來觸發(fā)重新渲染。
- 可以使用這樣的 {...obj, something: 'newValue'} 對(duì)象展開語法來創(chuàng)建對(duì)象的拷貝。
- 對(duì)象的展開語法是淺層的:它的復(fù)制深度只有一層。
- 想要更新嵌套對(duì)象,需要從更新的位置開始自底向上為每一層都創(chuàng)建新的拷貝。
- 想要減少重復(fù)的拷貝代碼,可以使用 Immer。
到此這篇關(guān)于React 18 更新 state 中的對(duì)象的文章就介紹到這了,更多相關(guān)React更新 state 對(duì)象內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
useEffect?返回函數(shù)執(zhí)行過程源碼解析
這篇文章主要為大家介紹了useEffect?返回函數(shù)執(zhí)行過程源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04react?hooks中的useState使用要點(diǎn)
這篇文章主要為大家介紹了react?hooks中的useState使用要點(diǎn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09React+Antd 實(shí)現(xiàn)可增刪改表格的示例
這篇文章主要介紹了React+Antd實(shí)現(xiàn)可增刪改表格的示例,幫助大家更好的理解和學(xué)習(xí)使用React,感興趣的朋友可以了解下2021-04-04基于CSS實(shí)現(xiàn)MaterialUI按鈕點(diǎn)擊動(dòng)畫并封裝成 React 組件
筆者先后開發(fā)過基于vue,react,angular等框架的項(xiàng)目,碧如vue生態(tài)的elementUI, ant-design-vue, iView等成熟的UI框架, react生態(tài)的ant-design, materialUI等,這些第三方UI框架極大的降低了我們開發(fā)一個(gè)項(xiàng)目的成本和復(fù)雜度,使開發(fā)者更專注于實(shí)現(xiàn)業(yè)務(wù)邏輯和服務(wù)化2021-11-11詳細(xì)談?wù)凴eact中setState是一個(gè)宏任務(wù)還是微任務(wù)
學(xué)過react的人都知道,setState在react里是一個(gè)很重要的方法,使用它可以更新我們數(shù)據(jù)的狀態(tài),下面這篇文章主要給大家介紹了關(guān)于React中setState是一個(gè)宏任務(wù)還是微任務(wù)的相關(guān)資料,需要的朋友可以參考下2021-09-09React-Route6實(shí)現(xiàn)keep-alive效果
本文主要介紹了React-Route6實(shí)現(xiàn)keep-alive效果,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧<BR>2022-06-06React.InputHTMLAttributes實(shí)踐和注意事項(xiàng)
文章討論了如何在封裝組件中使用React.InputHTMLAttributes,以及如何通過 {...inputProps} 動(dòng)態(tài)傳遞屬性,最后,文章總結(jié)了使用React.InputHTMLAttributes的最佳實(shí)踐和注意事項(xiàng),感興趣的朋友一起看看吧2024-11-11ReactRouterV6如何獲取當(dāng)前路由參數(shù)
這篇文章主要介紹了ReactRouterV6如何獲取當(dāng)前路由參數(shù)問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03