維持 React 資料流可靠性的重要關鍵:immutable state
什麼是 mutate?
在程式設計中,mutate(改變) 指的是直接修改某個物件或陣列的內容,而不是創建一個新的物件或陣列來表示改變後的狀態。簡單來說,當你對一個變數的內容進行修改,像是改變物件的屬性值或陣列的元素,這個行為就是 mutate。
在 React 中,state 是用來管理元件的狀態,React 期望 state 是 不可變的(immutable),這意味著你不應該直接修改 state 的內容,而是應該創建一個新的 state 來反映改變。直接修改 state(mutate)會導致問題,例如 React 無法正確檢測到狀態變化,進而影響元件的重新渲染。
為什麼 mutate 是問題?
React 使用 虛擬 DOM(Virtual DOM) 來比較新舊狀態,決定是否需要更新畫面。如果直接 mutate state,React 可能無法察覺到變化,因為物件或陣列的參考(reference)並未改變。這會導致畫面更新不正確,甚至引發 bug。
Mutate 的範例
以下是一個 mutate state 的錯誤示範:
import React, { useState } from "react";
function Counter() {
  const [user, setUser] = useState({ name: "小明", age: 20 });
  const handleAgeChange = () => {
    // 直接修改 state(mutate)
    user.age = user.age + 1; // 錯誤!直接改變了 user 物件
    setUser(user); // 即使呼叫 setUser,React 可能無法檢測到變化
  };
  return (
    <div>
      <p>姓名:{user.name}</p>
      <p>年齡:{user.age}</p>
      <button onClick={handleAgeChange}>增加年齡</button>
    </div>
  );
}
export default Counter;
問題說明:
- 
在 handleAgeChange 中,我們直接修改了 user.age,這是對 state 物件的 mutate。
- 
即使呼叫了 setUser(user),因為 user 的參考並未改變,React 不會認為 state 有變化,因此畫面不會更新。
- 
這可能導致年齡值實際上改變了,但畫面上顯示的年齡不會更新。 
保持 state 的 immutable
什麼是 immutable state?
Immutable state 指的是在更新 state 時,永遠不直接修改原本的物件或陣列,而是創建一個新的物件或陣列來表示更新後的狀態。這樣可以確保 React 正確檢測到狀態變化,並觸發必要的重新渲染。
為什麼要保持 state 的 immutable?
- 
確保 React 能檢測變化:React 依靠物件參考的比較來判斷 state 是否改變。如果 state 是 immutable 的,每次更新都會產生一個新的參考,React 就能正確檢測到變化。 
- 
提高程式碼可預測性:Immutable state 讓程式碼行為更可預測,避免因直接修改物件而引發的副作用(side effects)。 
- 
方便除錯:Immutable state 讓你更容易追蹤狀態的變化,因為每個狀態都是獨立的快照。 
- 
支援時間旅行(Time Travel Debugging):在像 Redux 這樣的狀態管理庫中,immutable state 是實現時間旅行除錯的基礎。 
如何保持 state 的 immutable?
以下是幾個在 React 中保持 state immutable 的常用方法:
1. 使用展開運算子(Spread Operator)
展開運算子 (...) 可以用來複製物件或陣列,創建一個新的副本,然後再對副本進行修改。
範例:更新物件
import React, { useState } from "react";
function Counter() {
  const [user, setUser] = useState({ name: "小明", age: 20 });
  const handleAgeChange = () => {
    // 使用展開運算子創建新物件
    const newUser = { ...user, age: user.age + 1 };
    setUser(newUser); // 更新 state
  };
  return (
    <div>
      <p>姓名:{user.name}</p>
      <p>年齡:{user.age}</p>
      <button onClick={handleAgeChange}>增加年齡</button>
    </div>
  );
}
export default Counter;
說明:
- 
{ ...user, age: user.age + 1 }創建了一個新的物件,複製了 user 的所有屬性,並更新 age。
- 
因為傳遞給 setUser 的是新物件,React 會檢測到變化並重新渲染元件。 
範例:更新陣列
import React, { useState } from "react";
function TodoList() {
  const [todos, setTodos] = useState(["買牛奶", "寫作業"]);
  const addTodo = () => {
    // 使用展開運算子創建新陣列
    const newTodos = [...todos, "運動"];
    setTodos(newTodos); // 更新 state
  };
  return (
    <div>
      <ul>
        {todos.map((todo, index) => (
          <li key={index}>{todo}</li>
        ))}
      </ul>
      <button onClick={addTodo}>新增待辦事項</button>
    </div>
  );
}
export default TodoList;
說明:
- 
[...todos, '運動']創建了一個新陣列,包含原有的 todos 和新項目。
- 
這確保了 todos 的 immutable 性,避免直接修改原陣列。 
2. 使用 map 或 filter 處理陣列
當需要修改陣列中的某個元素或刪除元素時,可以使用 map 或 filter 來創建新陣列。
範例:更新陣列中的某個元素
import React, { useState } from "react";
function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: "買牛奶", done: false },
    { id: 2, text: "寫作業", done: false },
  ]);
  const toggleTodo = (id) => {
    // 使用 map 創建新陣列
    const newTodos = todos.map((todo) =>
      todo.id === id ? { ...todo, done: !todo.done } : todo
    );
    setTodos(newTodos);
  };
  return (
    <div>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <span
              style={{ textDecoration: todo.done ? "line-through" : "none" }}
            >
              {todo.text}
            </span>
            <button onClick={() => toggleTodo(todo.id)}>
              {todo.done ? "取消完成" : "標記完成"}
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}
export default TodoList;
說明:
- 
map 方法遍歷 todos 陣列,當找到對應的 id 時,使用展開運算子創建一個新物件,更新 done 屬性。 
- 
其他未匹配的元素保持不變,這樣確保了陣列的 immutable 性。 
範例:刪除陣列中的元素
import React, { useState } from "react";
function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: "買牛奶", done: false },
    { id: 2, text: "寫作業", done: false },
  ]);
  const deleteTodo = (id) => {
    // 使用 filter 創建新陣列
    const newTodos = todos.filter((todo) => todo.id !== id);
    setTodos(newTodos);
  };
  return (
    <div>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            {todo.text}
            <button on上Click={() => deleteTodo(todo.id)}>刪除</button>
          </li>
        ))}
      </ul>
    </div>
  );
}
export default TodoList;
說明:
- 
filter 方法返回一個新陣列,僅包含不符合條件的元素(即 id 不等於指定值的元素)。 
- 
這樣不會修改原始陣列,保持了 immutable 性。 
3. 使用物件的 assign 方法
如果你不喜歡使用展開運算子,也可以使用 Object.assign 來複製物件。
範例:
import React, { useState } from "react";
function Counter() {
  const [user, setUser] = useState({ name: "小明", age: 20 });
  const handleAgeChange = () => {
    // 使用 Object.assign 創建新物件
    const newUser = Object.assign({}, user, { age: user.age + 1 });
    setUser(newUser);
  };
  return (
    <div>
      <p>姓名:{user.name}</p>
      <p>年齡:{user.age}</p>
      <button onClick={handleAgeChange}>增加年齡</button>
    </div>
  );
}
export default Counter;
說明:
- 
Object.assign({}, user, { age: user.age + 1 })創建一個新物件,複製 user 的屬性並更新 age。
- 
這也是一種保持 immutable 的方式,但展開運算子通常更簡潔。 
4. 使用 Immer(進階)
如果你的 state 結構很複雜,手動保持 immutable 可能會變得繁瑣。這時可以使用 Immer 函式庫,讓你用看起來像直接修改的方式來編寫程式碼,但實際上它會幫你生成 immutable 的結果。
安裝 Immer:
npm install immer
範例:使用 Immer 更新 state
import React, { useState } from "react";
import produce from "immer";
function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: "買牛奶", done: false },
    { id: 2, text: "寫作業", done: false },
  ]);
  const toggleTodo = (id) => {
    // 使用 produce 來更新 state
    const newTodos = produce(todos, (draft) => {
      const todo = draft.find((t) => t.id === id);
      todo.done = !todo.done; // 直接修改 draft,Immer 會生成新陣列
    });
    setTodos(newTodos);
  };
  return (
    <div>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <span
              style={{ textDecoration: todo.done ? "line-through" : "none" }}
            >
              {todo.text}
            </span>
            <button onClick={() => toggleTodo(todo.id)}>
              {todo.done ? "取消完成" : "標記完成"}
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}
export default TodoList;
說明:
- 
produce 函數接受當前的 state 和一個 draft(草稿)函數,你可以在 draft 中直接修改物件或陣列。 
- 
Immer 會自動生成一個新的 immutable state,減少手動複製的麻煩。 
總結
- 
什麼是 mutate:直接修改物件或陣列的內容,會導致 React 無法正確檢測狀態變化,引發畫面更新問題。 
- 
保持 immutable 的方法: - 
使用展開運算子 (...) 複製物件或陣列。 
- 
使用 map 或 filter 處理陣列更新。 
- 
使用 Object.assign 複 Dz 製物件。 
- 
使用 Immer 簡化複雜 state 的更新。 
 
- 
- 
好處:保持 state 的 immutable 性可以讓 React 正確檢