---
# System prepended metadata

title: 為什麼 React 中的 state 必須是 immutable？
tags: [JavaScript, React]

---

# 為什麼 React 中的 state 必須是 immutable？

> 備注：
> immutable = 資料不可變性
> mutable = 資料可變性

## 從變數說起：理解記憶體的秘密

###  Q1：請問會印出 `x` 還是 `5` 呢？

```js
let x = 5;
let y = x;
y = 10;
console.log(x); // ?
```

**A：5。**

當 `let y = x` 執行時，是將 `x` 的值（5）**複製**到變數 `y` 的記憶體位置。此時 `x` 和 `y` 是兩個獨立的變數，各自儲存自己的數值。

>  `x` 就像你的筆記本，第一頁寫著 `5`
>  `y` 是你朋友的筆記本，他抄了一份 `5`
>  當朋友把他的筆記本改成 `10`，你的筆記本還是 `5`

### Q2：為什麼 `array1` 的第一個元素會變成 `5`？

```js
const array1 = [1, 2, 3];
const array2 = array1;
array2[0] = 5;
console.log(array1); // [5, 2, 3]
```

**A：因為這是參考型別的「共享記憶體」行為。**

`array2 = array1` 是把 `array1` 的\*\*記憶體參考（reference）\*\*賦值給 `array2`，代表兩者指向的是同一個儲存資料的位址。

> `array1` 是一把倉庫的鑰匙
> `array2` 是你複製出來的鑰匙
> 這兩把鑰匙都能打開同一間倉庫，改變倉庫內容後，兩者都會看到變化

## 原始型別 vs 參考型別

JavaScript 中，資料型別分為兩大類：

| 類型   | 特性  | 範例                            |
| ---- | --- | ----------------------------- |
| 原始型別 | 不可變 | Number, String, Boolean, etc. |
| 參考型別 | 可變  | Object, Array, Function, etc. |

原始型別是「值的複製」，參考型別是「記憶體參考的複製」。

> 特殊案例：`null` 是原始型別，但 `typeof null === "object"` 是 JavaScript 歷史的 bug。

## 資料不變性（Immutability）：不是不能變，而是不直接改

```js
const user = { name: "Alice", age: 25 };
const updatedUser = user;
updatedUser.age = 26;

console.log(user.age); // !!變成 26
```

在這段範例中，我們**以為**複製了一份 `user` 資料，但其實只是複製了它的參考。當我們修改 `updatedUser` 時，其實也是在改原本的 `user`。

這就是參考型別的陷阱，會導致：

* 意料之外的副作用
* 其他使用者無端被影響
* 無法追蹤哪些資料真的被改動

### ✅ 正確作法：創建新資料

```js
const user = { name: "Alice", age: 25 };
const updatedUser = { ...user, age: 26 };

console.log(user.age);        // 25
console.log(updatedUser.age); // 26
```

透過展開運算子（spread），你就創造了一份新的資料，兩者互不影響。


## React 的痛點：如何知道什麼時候該重新渲染？

React 中最關鍵的設計之一，就是**狀態改變 → UI 更新（Re-render）**。

為了讓這個機制運作得輕量又快速，React 選擇了一條重要的設計原則：

> 只要 state 是不可變的，React 就能用最小的代價判斷「資料有沒有改變」


### React 中 setState 的判斷機制

React 使用 `Object.is()` 來判斷 state 是否改變：

```js
const setState = (newValue) => {
  if (!Object.is(currentState, newValue)) {
    currentState = newValue;
    scheduleReRender(); // -> 觸發畫面更新
  }
};
```

這個判斷不會比較資料內容，而是**直接比較記憶體位置（reference）是否相同**。


### 為什麼不用深層比較（deep equality）？

| 比較方式          | 優點              | 缺點                 |
| ------------- | --------------- | ------------------ |
| `Object.is()` | 快速、效能好、記憶體低耗    | 只判斷 reference，不看內容 |
| 深層比較（deep）    | 可以精準比較所有層資料是否一致 | 非常慢、複雜、效能差         |

所以 React 的立場是：

> ❗你幫我維持資料不可變，我就幫你判斷是否該重繪。

### 錯誤範例：只改內容，沒有改 reference

```js
function MyComponent() {
  const [user, setUser] = useState({ name: "Alice", age: 25 });

  const badUpdate = () => {
    user.age = 26;
    setUser(user); // 參考沒變，React 不會 re-render
  };
}
```

這段程式碼表面上資料有改，但因為記憶體位置沒變，React 不會覺得「你真的更新了 state」，所以畫面不會更新。


## 正確更新方式

```tsx
function MyComponent() {
  const [number, setNumber] = useState(0);
  const [user, setUser] = useState({ name: "Alice", age: 25 });

  const goodUpdate1 = () => setNumber(5); // 原始型別，自然 immutable
  const goodUpdate2 = () => setUser({ ...user, age: 26 }); // 新 reference
}
```

## 為什麼 React 使用 `Object.is()`？

React 的設計目標是：**可預測、效能穩定、維護簡單**。使用 `Object.is()` 有以下好處：

1. **效能極高**：Reference 比較速度快，不需深層遞迴。
2. **搭配 immutable 資料結構效果最佳**：只要你創造新資料，React 就能瞬間知道有變動。

> 記得：**不變性不是限制你不能修改資料，而是強迫你用更安全的方式修改它。**

## 淺拷貝（Shallow Copy）還是手動深層更新？

在 React 中，僅僅知道要「創建新物件」還不夠，更關鍵的是要理解：你更新的路徑上，每一層都必須是新的參考。 這就是不可變性 (Immutability) 的核心精神。

來看一個實務上常見的巢狀資料更新範例：

```ts
// ✅ 正確寫法：沿途建立新參考
select: (data) => {
  return {
    // 1. 建立新的頂層物件
    ...data,
    // 2. 建立新的 data 陣列
    data: data?.data?.map((item) => ({
      // 3. 建立新的 item 物件
      ...item,
      division: { label: item.divName, value: item.divNoReg },
    })),
  };
}
```

### 拆解分析：

這段程式碼完美地遵循了不可變性原則，讓我們一層層來看：

1. { ...data }：這是一個淺拷貝。它創建了一個全新的最外層物件。然而，在這一刻，它內部的 data.data 屬性仍然指向舊的陣列參考。
2. data.data.map(...)：.map() 方法永遠會回傳一個全新的陣列。我們用這個新陣列覆蓋了 data 屬性，確保了第二層也是新的。
3. { ...item, ... }：在 map 的每一次迭代中，我們都用展開語法創建了一個全新的 item 物件，而不是去修改原始的 item。這保證了陣列中的每個元素也都是新的參考。
4. 
結論： 
    因為從根物件到目標資料的每一層都換上了新的參考，React 的 `Object.is` 比較能夠輕鬆偵測到變化，從而正確地觸發重新渲染。

### 錯誤寫法：一個隱藏的陷阱

```ts
// ❌ 錯誤寫法：直接修改 (mutate) 了原始資料
data: data?.data?.map((item) => {
  item.division = { label: item.divName, value: item.divNoReg }; // 危險！
  return item;
});
```        

這個寫法是個非常常見的陷阱。它之所以看似有效（觸發了重新渲染），純粹是因為 `.map()` 回傳了一個新陣列參考。但它同時也污染了原始的 state，犯了 React 的大忌：直接修改 (mutation)。

`item` 是 `data.data` 原始陣列中物件的直接參考。`item.division = ...` 這行程式碼像一把手術刀，直接劃開並修改了原始物件。這種做法會破壞 React 的單向資料流和可預測性，最終導致：

- 無法預期的 UI 行為：元件可能在不該更新時更新，或在該更新時沒反應。
- 效能優化機制失效：`React.memo`、`useMemo` 或 `shouldComponentUpdate` 都依賴乾淨的參考比較，直接修改會讓它們全部失靈。
-難以追蹤的 Bug：當 state 被四處意外修改，除錯將會變成一場惡夢。


## 總結：

1. **理解型別差異**
   原始型別值會複製，參考型別只複製記憶體參考。

2. **Immutable 的精神**
   改資料前，請先創造新的記憶體位置。

3. **React 重新渲染依據**
   使用 `Object.is()` 比較 reference，只要記憶體位置沒改，就不會 re-render。

4. **警惕淺拷貝陷阱**
   表面看似「改了資料」，實際卻沒產生新 reference，會讓 React 判斷失誤。


## 參考資料：
1. [Primitive vs Reference Data Types in JavaScript](https://www.freecodecamp.org/news/primitive-vs-reference-data-types-in-javascript/)
2. [Object.is()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is)
3. [維持 React 資料流可靠性的核心關鍵：Immutable state](https://ithelp.ithome.com.tw/articles/10301603)