React 開發者必學!Immer.js 簡化不可變資料處理的最佳利器

作者: Calpa Liu
字數:3254
出版:April 14, 2025
分類: JavaScript 前端開發 狀態管理 React.js Redux

在現代前端開發中,不可變性 (Immutability) 已成為處理狀態的重要範式,尤其是在 React、Redux 等生態系統中。Immer.js 作為一個輕量級的庫,提供了一種優雅的方式來處理 JavaScript 中的不可變狀態,並在 2019 年贏得了 React 開源"年度突破"獎和 JavaScript 開源"最具影響力貢獻"獎。本文將深入探討 Immer.js 的優勢及其在實際開發中的應用。

Immer.js 簡介

Immer(德語中”永遠”的意思)是一個小型的 JavaScript 庫,允許開發者以一種更便捷的方式處理不可變狀態。Redux 維護者 Mark Erikson 曾評價道:“作為 JS 開發者,Immer 是改變生活的,我甚至沒有誇張:) 它和 Prettier 一樣令人驚嘆,讓人不禁想’這個庫太棒了,我以前怎麼沒有用它?’”

Immer 的核心理念是通過一個臨時的 草稿狀態 (draftState) 來處理數據更新,這個草稿狀態是當前狀態的代理 (Proxy)。開發者可以像操作可變數據一樣直接修改這個草稿,而 Immer 會基於這些修改自動產生新的不可變狀態,同時保持原始狀態不變。

安裝與基本使用

Immer.js 可以通過 npm 或 yarn 安裝:

# 使用 npm
npm install immer

# 或使用 yarn
yarn add immer

安裝完成後,你可以在你的 JavaScript 或 TypeScript 文件中導入 Immer:

import produce from 'immer'

// 或者如果你使用 CommonJS
// const produce = require('immer').produce

Immer 的核心函數是 produce,它允許你創建下一個不可變狀態:

const baseState = [
  { title: "學習 Immer", done: false },
  { title: "使用 Immer", done: false }
]

const nextState = produce(baseState, draft => {
  draft[1].done = true
  draft.push({ title: "分享 Immer", done: false })
})

console.log(baseState)  // 原始狀態不變
console.log(nextState)  // 新狀態包含更改

這個簡單的例子展示了 Immer 如何讓你以一種直觀的方式處理不可變更新。

Immer.js 的主要優勢

1. 使用熟悉的 JavaScript 語法

與其他不可變庫(如 ImmutableJS)不同,Immer 讓開發者能夠使用標準的 JavaScript 數據結構(物件、陣列、Set 和 Map)和操作方法。你不需要學習新的 API 或數據結構,可以直接使用你已經熟悉的 JavaScript 語法:

import produce from "immer"

const baseState = [
  { todo: "學習 TypeScript", done: true },
  { todo: "嘗試 Immer", done: false }
]

const nextState = produce(baseState, draftState => {
  draftState.push({ todo: "推薦給同事", done: false })
  draftState.done = true
})

2. 大幅減少樣板代碼

在傳統的不可變操作中,更新深層嵌套的數據結構需要大量的展開運算符 (...),導致代碼冗長難讀:

// 不使用 Immer 的情況
const updatedState = {
  ...state,
  user: {
    ...state.user,
    address: {
      ...state.user.address,
      city: 'Taipei'
    }
  }
}

// 使用 Immer 的情況
const updatedState = produce(state, draft => {
  draft.user.address.city = 'Taipei'
})

使用 Immer,代碼變得更加簡潔、直觀且易於維護。

3. 結構共享提高性能

Immer 通過”結構共享”(structural sharing) 技術優化性能,只為修改的部分創建新對象,而未修改的部分則與原始狀態共享。這減少了內存使用並提高了性能,特別是在處理大型狀態樹時。

具體來說,Immer 使用了一種稱為”寫時複製”(copy-on-write)的策略:

  1. 當你開始修改一個對象時,Immer 會創建該對象的淺拷貝。
  2. 如果你修改了這個對象的某個屬性,Immer 只會為這個屬性創建一個新的引用。
  3. 對於沒有被修改的屬性,Immer 會保持原有的引用不變。

這種方法確保了即使在大型、深層嵌套的數據結構中,也能保持高效的更新性能。例如:

const baseState = {
  user: {
    name: "John",
    age: 30,
    address: {
      city: "New York",
      country: "USA"
    }
  },
  settings: {
    theme: "dark",
    notifications: true
  }
}

const nextState = produce(baseState, draft => {
  draft.user.age = 31;
})

在這個例子中,nextStateuser 對象會是新的,但 settings 對象會與 baseState 共享相同的引用,因為它沒有被修改。這大大減少了不必要的對象創建,提高了性能和內存效率。

  1. Immer 會檢測”無操作”狀態變化,如果實際上沒有任何改變,則返回原始狀態,這可以避免不必要的重新渲染。

4. 自動檢測意外變異

Immer 會自動檢測對不可變對象的意外修改,並拋出錯誤,幫助開發者遵循不可變數據的最佳實踐。

5. 強類型支持

Immer 為 TypeScript 用戶提供了卓越的支持,大大增強了開發體驗:

  1. 自動類型推斷:Immer 能夠自動推斷 produce 函數的返回類型,減少了手動類型註解的需要。

  2. 完整的類型安全:與使用字符串路徑的選擇器相比,Immer 的方法保持了完整的類型安全,在編譯時就能捕獲潛在的類型錯誤。

  3. readonly 類型:Immer 提供了 immerable 類型,可以將類型標記為不可變,進一步增強類型檢查。

  4. 泛型支持:Immer 的 API 設計支持泛型,使得在複雜的數據結構中也能保持類型的精確性。

  5. IDE 智能提示:得益於其良好的類型定義,使用 Immer 時可以獲得出色的 IDE 自動完成和錯誤檢測支持。

這些特性使得 Immer 在大型 TypeScript 專案中特別有價值,能夠顯著提高代碼質量和開發效率。

6. 自動凍結對象

在開發模式下,Immer 會自動凍結 (freeze) 通過produce創建的數據結構,讓你獲得真正的不可變數據。這增加了代碼的安全性,防止意外修改。具體來說:

  1. 凍結操作是遞歸的,確保整個對象樹都是不可變的。
  2. 這種自動凍結只在開發環境中生效,不會影響生產環境的性能。
  3. 如果嘗試修改凍結的對象,JavaScript 會拋出錯誤,幫助開發者及早發現問題。
  4. 可以通過 setAutoFreeze(false) 來禁用這個功能,但通常不建議這樣做。

這個特性特別有助於捕獲那些可能導致副作用的意外修改,確保狀態的純粹性和可預測性。

7. 簡化處理深層嵌套數據

使用 Immer,對深層嵌套數據的更新變得極其簡單:

// 複雜數據結構的深層更新
const store = {
  users: new Map([
    ["17", {
      name: "Michel",
      todos: [
        { title: "買咖啡", done: false }
      ]
    }]
  ])
}

// 深層更新變得簡單
const nextStore = produce(store, draft => {
  draft.users.get("17").todos.done = true
})

與 Redux 結合使用

Immer 在 Redux 中特別有用,可以極大地簡化 reducer 的編寫:

// 不使用 Immer 的 reducer
const contactsReducer = (state = initialState, action) => {
  switch(action.type) {
    case "contacts/contactAdded":
      return {
        ...state,
        user: {
          ...state.user,
          contacts: {
            ...action.payload
          }
        }
      }
    default:
      return state
  }
}

// 使用 Immer 的 reducer
import produce from 'immer';

const contactsReducer = produce((draft, action) => {
  switch (action.type) {
    case "contacts/contactAdded":
      draft.user.contacts = {
        ...action.payload
      }
      break
    default:
      break
  }
})

原理

Immer 的核心運作方式,是透過 JavaScript 的 Proxy API 建立一個「草稿對象(draft)」作為原始狀態的代理。開發者可以像操作一般可變資料一樣修改這個草稿,Immer 會在背後自動追蹤所有變更。

當修改完成後,Immer 會根據這些操作,產生一個全新的不可變狀態對象,同時保留原始狀態不變,實現不可變性與開發便利性的雙重平衡。

這一切都由 Immer 的核心函數 produce 所驅動。它接收兩個參數:

  • 一個原始狀態(base state)
  • 一個修改函數(用來操作草稿狀態)

最終,produce 會回傳一個根據修改邏輯生成的新不可變狀態。

適用場景

Immer 通常用於以下場景:

  1. 狀態管理:在 React 中管理 UI 狀態,特別是在使用 Redux 時。Immer 可以大大簡化 reducer 的編寫,使得複雜的狀態更新變得直觀和易於維護。例如,在處理深層嵌套的狀態時,Immer 允許直接修改 draft 狀態,而不需要手動創建每一層的新對象。

  2. 數據處理:在處理複雜的數據結構時,如 JSON 補丁操作或狀態更新。Immer 特別適合處理深層嵌套的對象或大型數組,它能夠在保持不可變性的同時,提供一種直觀的方式來更新這些複雜結構。

  3. 測試:在測試中生成不同的狀態,特別是在需要模擬多種狀態變化的場景。Immer 的 produce 函數可以輕鬆創建多個狀態變體,而不會影響原始狀態,這使得編寫單元測試和集成測試變得更加容易和可靠。

  4. 數據同步:在需要同步多個狀態或數據源時,如在多個組件間同步狀態。Immer 的不可變更新模式確保了數據的一致性,同時簡化了跨組件的狀態管理邏輯。

  5. 性能優化:在需要優化渲染性能的場景中。由於 Immer 只創建必要的新對象,它可以幫助減少不必要的重渲染,特別是在使用 React.memo 或 shouldComponentUpdate 進行性能優化時。

  6. 協作開發:在大型團隊協作的項目中。Immer 的簡單 API 和直觀的使用方式使得團隊成員更容易理解和維護彼此的代碼,減少了由於不當的狀態修改而導致的錯誤。

性能考量

雖然 Immer 在大多數情況下性能良好,但有幾點需要注意:

  1. Immer 使用 ES6 Proxy API 實現,與手寫 reducer 相比約慢 2-3 倍,但這在實際應用中通常可以忽略。

  2. 對於非常大的數據結構,Immer 可能會有些性能影響。但 Immer 提供了一些性能優化技巧,如預先凍結數據和將produce函數盡可能提升。

  3. Immer 會檢測”無操作”狀態變化,如果實際上沒有任何改變,則返回原始狀態,這可以避免不必要的重新渲染。

與其他不可變資料庫的比較

在處理不可變狀態的工具選擇上,Immer 是目前最受歡迎的解法之一。以下將從學習曲線、整合性、效能與型別支援等面向,分別比較 Immer 與 ImmutableJS、Mutative 兩個主流替代方案。

Immer 與 ImmutableJS 的比較

API 學習曲線方面,Immer 使用原生 JavaScript 的操作方式,例如物件與陣列的直接修改,因此幾乎沒有學習門檻。而 ImmutableJS 則提供一套專有的資料結構與方法,如 Map()、List() 等,需要額外學習,對初學者與現有團隊而言學習曲線較陡峭。

與現有程式碼整合方面,Immer 可無縫融入現有的 React 或 JavaScript 專案中,不需進行任何資料格式轉換。而 ImmutableJS 則常常需要在應用程式的邊界處來回轉換原生資料與 Immutable 資料結構,增加了整合與維護的複雜度。

效能表現方面,Immer 在處理中小型資料結構時表現優異,特別適合常見的 UI 狀態管理場景。但若處理的是極大型或深度嵌套的資料結構,ImmutableJS 採用的持久化資料結構可能會在效能上更具優勢。

記憶體使用方面,Immer 採用「寫時複製」(copy-on-write)並搭配結構共享策略,能有效節省記憶體。而 ImmutableJS 則透過結構共享實作出類似功能,但在部分場景下記憶體利用可能更為高效。

Immer 與 Mutative 的比較

易用性方面,Immer 的 API 設計直觀,使用方式接近原生 JavaScript,因此上手快速。而 Mutative 則偏向進階使用者,提供較多控制能力,但 API 較為複雜,需要更多學習與實驗。

效能方面,Immer 在大多數實務開發情境下效能已足夠穩定。然而在需要頻繁更新大量狀態的高效能場景中,Mutative 可能擁有些微優勢,特別是在需要精細掌控變更的情況下。

生態系統整合方面,Immer 已廣泛應用於 React 和 Redux 中,有豐富的範例、套件與社群支援。相對地,Mutative 的整合與資源較少,開發者可能需額外處理相容性問題。

TypeScript 支援方面,Immer 提供優異的型別推斷與靜態檢查,能在開發階段提供高度型別保障。而 Mutative 雖然具備基本型別支援,但相對而言較不完整,需更多手動干預。

結論

Immer.js 為 JavaScript 開發者提供了一種簡單、直觀且高效的方式來處理不可變數據。它通過允許開發者以可變的方式操作不可變數據,極大地簡化了代碼並提高了可讀性。特別是在 React 和 Redux 應用中,Immer 可以顯著減少樣板代碼並使狀態更新邏輯更清晰。

雖然在極端情況下可能有一些性能開銷,但 Immer 在大多數實際應用場景中都表現良好,並且提供了多種優化選項。對於現代 JavaScript 開發者來說,Immer 是處理不可變狀態的絕佳工具,值得加入您的開發技術棧。

無論您是前端開發新手還是經驗豐富的開發者,Immer 都能幫助您寫出更簡潔、更可維護的代碼,同時遵循不可變數據的最佳實踐。正如其官方口號所說:Immer:以簡單的方式實現不可變性

感謝您閱讀我的文章。歡迎隨時分享你的想法。
關於 Calpa

Calpa 擅長使用 TypeScriptReact.jsVue.js 建立 Responsive Website。

他積極參與開源社區,曾在 2019 年的香港開源大會上擔任講者,提供工作經驗和見解。此外,他也在 GitHub 上公開分享個人博客程式碼,已獲得超過 300 顆星星和 60 個分支的支持。

他熱愛學習新技術,並樂意分享經驗。他相信,唯有不斷學習才能跟上快速演變的技術環境。