Immutable update
物件資料的 immutable update 方法
什麼是 immutable update? 在 React 中,為了確保狀態(state)管理的正確性,應該避免直接修改原始物件(mutable update),而是創建一個新物件來更新資料,保持原始資料不變。這稱為 immutable update,有助於 React 檢測狀態變化並觸發重新渲染。
常見方法:
-
使用展開運算子(Spread Operator): 展開運算子 (...) 可以複製物件的屬性,並在更新時添加或修改特定屬性。
-
Object.assign: 將多個物件合併,創建新物件。
範例程式碼:
假設我們有一個 React 元件,管理一個物件狀態:
import React, { useState } from "react";
function ObjectUpdateExample() {
const [user, setUser] = useState({
name: "小明",
age: 25,
email: "xiaoming@example.com",
});
// 更新 name 的函式
const updateName = () => {
// 使用展開運算子創建新物件
setUser({
...user,
name: "小華",
});
};
// 更新多個屬性的函式
const updateMultiple = () => {
// 使用展開運算子更新多個屬性
setUser({
...user,
name: "小強",
age: 30,
});
};
// 使用 Object.assign 更新
const updateWithAssign = () => {
setUser(Object.assign({}, user, { email: "xiaoqiang@example.com" }));
};
return (
<div>
<h2>物件資料的 Immutable Update</h2>
<p>姓名: {user.name}</p>
<p>年齡: {user.age}</p>
<p>Email: {user.email}</p>
<button onClick={updateName}>更新姓名</button>
<button onClick={updateMultiple}>更新姓名與年齡</button>
<button onClick={updateWithAssign}>使用 Object.assign 更新 Email</button>
</div>
);
}
export default ObjectUpdateExample;
程式碼說明:
-
展開運算子:
-
...user
會複製 user 物件的所有屬性到新物件。 -
後面指定的屬性(如
name: '小華'
)會覆蓋原有的屬性。 -
這是 React 中最常見的 immutable update 方法,因為簡單且直觀。
-
-
Object.assign:
-
第一個參數是目標物件(空物件 ),後面是來源物件。
-
將 user 和新屬性合併,創建新物件。
-
-
為什麼要用 immutable update?
-
直接修改
user.name = '小華'
會改變原始物件,React 可能無法檢測到變化,導致畫面不更新。 -
使用新物件確保狀態變化可被 React 正確追蹤。
-
注意事項:
-
展開運算子僅進行淺層複製(shallow copy)。
-
確保每次更新都傳遞新物件給 setUser,以觸發 React 重新渲染。
陣列資料的 immutable update 方法
陣列的 immutable update: 陣列也是參考型別,直接修改陣列(如 push、splice)會改變原始資料,必須使用 immutable 方法創建新陣列。
常見方法:
-
展開運算子:複製陣列並添加或修改元素。
-
陣列方法:如 map、filter、slice 等,返回新陣列。
-
concat:合併陣列,創建新陣列。
範例程式碼:
假設我們有一個 React 元件,管理一個陣列狀態:
import React, { useState } from "react";
function ArrayUpdateExample() {
const [items, setItems] = useState(["蘋果", "香蕉", "橘子"]);
// 添加新元素
const addItem = () => {
setItems([...items, "葡萄"]);
};
// 移除特定元素
const removeItem = (index) => {
setItems(items.filter((_, i) => i !== index));
};
// 更新特定元素
const updateItem = (index) => {
setItems(items.map((item, i) => (i === index ? `${item} (已更新)` : item)));
};
// 使用 concat 添加元素
const addItemWithConcat = () => {
setItems(items.concat("芒果"));
};
return (
<div>
<h2>陣列資料的 Immutable Update</h2>
<ul>
{items.map((item, index) => (
<li key={index}>
{item}
<button onClick={() => updateItem(index)}>更新</button>
<button onClick={() => removeItem(index)}>移除</button>
</li>
))}
</ul>
<button onClick={addItem}>添加葡萄</button>
<button onClick={addItemWithConcat}>使用 concat 添加芒果</button>
</div>
);
}
export default ArrayUpdateExample;
程式碼說明:
-
添加元素:
-
使用
...items
複製陣列,然後添加新元素(如 '葡萄')。 -
concat 方法也能達到相同效果,返回新陣列。
-
-
移除元素:
- 使用 filter 方法創建新陣列,過濾掉指定索引的元素。
-
更新元素:
- 使用 map 方法遍歷陣列,僅更新指定索引的元素,其他元素保持不變。
-
為什麼不用 push 或 splice?
- 這些方法會直接修改原始陣列,違反 immutable 原則,可能導致 React 無法檢測變化。
注意事項:
-
確保使用返回新陣列的方法(如 map、filter、slice、concat)。
-
避免使用 push、pop、shift、unshift 等會修改原始陣列的方法。
-
陣列中的物件若為巢狀結構,更新時需注意深層複製。
巢狀式參考型別的複製誤解
什麼是巢狀參考型別? 當物件或陣列內包含其他物件或陣列時,稱為巢狀結構。使用展開運算子或 Object.assign 進行複製時,只會進行淺層複製,巢狀物件仍指向原始參考,修改巢狀物件會影響原始資料。
常見誤解:
-
誤以為展開運算子或
Object.assign
會複製所有層級的資料。 -
直接修改巢狀物件,導致意外改變原始資料。
解決方法:
-
深層複製(Deep Copy):
-
使用
JSON.parse(JSON.stringify(obj))
(簡單但有局限性)。 -
使用第三方庫如 lodash 的
_.cloneDeep
。
-
-
手動複製巢狀結構:
- 針對每個巢狀層級使用展開運算子。
-
結構化複製(Structured Cloning):
- 瀏覽器支援 structuredClone(較新 API)。
範例程式碼:
假設我們有一個巢狀物件的 React 元件:
import React, { useState } from "react";
function NestedObjectExample() {
const [user, setUser] = useState({
name: "小明",
info: {
age: 25,
address: {
city: "台北",
zip: "100",
},
},
});
// 錯誤示範:直接修改巢狀物件
const wrongUpdate = () => {
const newUser = { ...user };
newUser.info.address.city = "台中"; // 這會改變原始物件!
setUser(newUser);
};
// 正確示範:手動深層複製
const correctUpdate = () => {
setUser({
...user,
info: {
...user.info,
address: {
...user.info.address,
city: "台中",
},
},
});
};
// 使用 JSON 深層複製
const updateWithJSON = () => {
const newUser = JSON.parse(JSON.stringify(user));
newUser.info.address.city = "高雄";
setUser(newUser);
};
return (
<div>
<h2>巢狀物件的 Immutable Update</h2>
<p>姓名: {user.name}</p>
<p>年齡: {user.info.age}</p>
<p>城市: {user.info.address.city}</p>
<p>郵遞區號: {user.info.address.zip}</p>
<button onClick={wrongUpdate}>錯誤更新城市</button>
<button onClick={correctUpdate}>正確更新城市</button>
<button onClick={updateWithJSON}>使用 JSON 更新城市</button>
</div>
);
}
export default NestedObjectExample;
程式碼說明:
-
錯誤示範:
-
使用
...user
只複製第一層屬性,info.address
仍指向原始物件。 -
修改
newUser.info.address.city
會意外改變原始 user 物件。
-
-
正確示範:
-
對每一層巢狀結構使用展開運算子,確保創建全新物件。
-
這樣修改不會影響原始物件。
-
-
JSON 深層複製:
JSON.parse(JSON.stringify(obj))
會複製所有層級,但無法處理函式、Date 等特殊物件。
-
structuredClone(選項):
- 如果瀏覽器支援,可使用
structuredClone(user)
進行深層複製。
- 如果瀏覽器支援,可使用
注意事項:
-
淺層複製的風險:只複製第一層屬性,巢狀物件仍共用參考。
-
JSON 限制:無法複製函式、undefined、Symbol 等,需視情況選擇。
-
效能考量:深層複製可能影響效能,巢狀層級不多時優先使用展開運算子。
-
第三方庫:若專案使用 lodash,可使用
_.cloneDeep
進行深層複製。
總結與建議:
-
物件更新:優先使用展開運算子,簡單且直觀;若需合併多物件,可用
Object.assign
。 -
陣列更新:使用 map、filter、concat 或展開運算子,避免直接修改陣列。
-
巢狀結構:注意淺層複製的限制,手動展開每一層或使用深層複製方法。
-
React 最佳實務:始終傳遞新物件/陣列給 setState,確保狀態變化可被檢測。