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,確保狀態變化可被檢測。