昨天學習了 React 的 batching 的概念、「替代」和「做」的區別、當次渲染的邏輯,今天要來學習如何在 state 中更新物件。學習前讓我們再次回憶起之前學過的 useState 語法。
const [state, setState] = useState(initialState);
回顧完這些東西,實際來看看例子。
首先我們先預設有傳入 useState 的初始值為一個物件,物件裡指定了座標 x: 0
與 y:0 ,也就是座標的起始值。
const [position, setPosition] = useState({
x: 0,
y: 0,
});
若我們想要讓指定的目標隨著滑鼠移動而更新座標,使用以下的作法能夠完成嗎?
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 的值,但這樣做是無效的,原因是我們在改動的是前一個渲染的資料。因為 positon 對應到的是當前的狀態,也就是作為初始值所傳入的參數,去改動它並不會有作用。在這邊做的動作稱為 mutation 這樣做就像「吃飽飯後再改變用餐順序一樣」。
這邊也有一個重要的觀念:將放入狀態的任何 JavaScript 物件視為唯讀(read-only)。
這個行不通的話,要做的事情就更明顯了,應該要想辦法觸法渲染才能達到目的,我們可以透過
創建新的物件並將它傳進可以觸發渲染的的 state setting function 當中,在這邊也就是 setPosition
。
onPointerMove={e => {
setPosition({
x: e.clientX,
y: e.clientY
});
}}
透過這樣做,我們告訴 React
positon
換成這個物件剛剛的方法是利用創建新物件來達到觸發渲染,但如果必須保留舊物件的資料的情況,譬如更新其中一欄的表格欄位,其餘欄位保持之前的資料,該如何處理?mutation 的方法在此一樣不能運作,若要保有舊的資料,勢必必須將舊的資料也一併傳入 setting function 當中。
像這樣子:
setPerson({
firstName: e.target.value, // New first name from the input
lastName: person.lastName,
email: person.email,
});
但若是 property 很多,在那邊手動複製,有點不妙。回憶起 JavaScript 的object spread的用法,就可以省去這些麻煩:
setPerson({
...person, // 複製舊的欄位
firstName: e.target.value, //但覆蓋這個欄位的資料
});
但是程式碼看起來還是有重複的地方:
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>
</>
);
}
改善這樣子的程式碼可以透過方括號來達到靈活(dynamic) 應對各個欄位的資料。
譬如可以改成這樣子
function handleChange(e) {
setPerson({
...person,
[e.target.name]: e.target.value,
});
}
之後在 return 的 jsx 中的onChange
prop 當中設定handleChange
即可。