Skip to main content

Immutable update

物件資料的 immutable update 方法

什麼是 immutable update? 在 React 中,為了確保狀態(state)管理的正確性,應該避免直接修改原始物件(mutable update),而是創建一個新物件來更新資料,保持原始資料不變。這稱為 immutable update,有助於 React 檢測狀態變化並觸發重新渲染。

常見方法:

  1. 使用展開運算子(Spread Operator): 展開運算子 (...) 可以複製物件的屬性,並在更新時添加或修改特定屬性。

  2. 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;

程式碼說明:

  1. 展開運算子

    • ...user 會複製 user 物件的所有屬性到新物件。

    • 後面指定的屬性(如 name: '小華')會覆蓋原有的屬性。

    • 這是 React 中最常見的 immutable update 方法,因為簡單且直觀。

  2. Object.assign

    • 第一個參數是目標物件(空物件 ),後面是來源物件。

    • 將 user 和新屬性合併,創建新物件。

  3. 為什麼要用 immutable update?

    • 直接修改 user.name = '小華' 會改變原始物件,React 可能無法檢測到變化,導致畫面不更新。

    • 使用新物件確保狀態變化可被 React 正確追蹤。

注意事項:

  • 展開運算子僅進行淺層複製(shallow copy)。

  • 確保每次更新都傳遞新物件給 setUser,以觸發 React 重新渲染。


陣列資料的 immutable update 方法

陣列的 immutable update: 陣列也是參考型別,直接修改陣列(如 push、splice)會改變原始資料,必須使用 immutable 方法創建新陣列。

常見方法:

  1. 展開運算子:複製陣列並添加或修改元素。

  2. 陣列方法:如 map、filter、slice 等,返回新陣列。

  3. 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;

程式碼說明:

  1. 添加元素

    • 使用 ...items 複製陣列,然後添加新元素(如 '葡萄')。

    • concat 方法也能達到相同效果,返回新陣列。

  2. 移除元素

    • 使用 filter 方法創建新陣列,過濾掉指定索引的元素。
  3. 更新元素

    • 使用 map 方法遍歷陣列,僅更新指定索引的元素,其他元素保持不變。
  4. 為什麼不用 push 或 splice?

    • 這些方法會直接修改原始陣列,違反 immutable 原則,可能導致 React 無法檢測變化。

注意事項:

  • 確保使用返回新陣列的方法(如 map、filter、slice、concat)。

  • 避免使用 push、pop、shift、unshift 等會修改原始陣列的方法。

  • 陣列中的物件若為巢狀結構,更新時需注意深層複製。


巢狀式參考型別的複製誤解

什麼是巢狀參考型別? 當物件或陣列內包含其他物件或陣列時,稱為巢狀結構。使用展開運算子或 Object.assign 進行複製時,只會進行淺層複製,巢狀物件仍指向原始參考,修改巢狀物件會影響原始資料。

常見誤解:

  • 誤以為展開運算子或 Object.assign 會複製所有層級的資料。

  • 直接修改巢狀物件,導致意外改變原始資料。

解決方法:

  1. 深層複製(Deep Copy)

    • 使用 JSON.parse(JSON.stringify(obj))(簡單但有局限性)。

    • 使用第三方庫如 lodash 的 _.cloneDeep

  2. 手動複製巢狀結構

    • 針對每個巢狀層級使用展開運算子。
  3. 結構化複製(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;

程式碼說明:

  1. 錯誤示範

    • 使用 ...user 只複製第一層屬性,info.address 仍指向原始物件。

    • 修改 newUser.info.address.city 會意外改變原始 user 物件。

  2. 正確示範

    • 對每一層巢狀結構使用展開運算子,確保創建全新物件。

    • 這樣修改不會影響原始物件。

  3. JSON 深層複製

    • JSON.parse(JSON.stringify(obj)) 會複製所有層級,但無法處理函式、Date 等特殊物件。
  4. structuredClone(選項):

    • 如果瀏覽器支援,可使用 structuredClone(user) 進行深層複製。

注意事項:

  • 淺層複製的風險:只複製第一層屬性,巢狀物件仍共用參考。

  • JSON 限制:無法複製函式、undefined、Symbol 等,需視情況選擇。

  • 效能考量:深層複製可能影響效能,巢狀層級不多時優先使用展開運算子。

  • 第三方庫:若專案使用 lodash,可使用 _.cloneDeep 進行深層複製。


總結與建議:

  • 物件更新:優先使用展開運算子,簡單且直觀;若需合併多物件,可用 Object.assign

  • 陣列更新:使用 map、filter、concat 或展開運算子,避免直接修改陣列。

  • 巢狀結構:注意淺層複製的限制,手動展開每一層或使用深層複製方法。

  • React 最佳實務:始終傳遞新物件/陣列給 setState,確保狀態變化可被檢測。