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

作者: Calpa Liu
字數:3254
出版:2025 年 4 月 14 日
分類: 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 個分支的支持。

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

熱門文章

最新文章

圖片管理中心
管理圖片資源
IP 查詢
快速查詢和定位 IP 地址的地理位置和相關信息
Python 運行器
無需後端、無需登入,只需打開瀏覽器即可運行 Python 代碼(由 Pyodide 提供支持)
封面圖生成器
自動創建適合各種平台的文章封面圖
原作(青山剛昌)產生器
一鍵創建原作(青山剛昌)的封面圖
日本色彩
探索和使用傳統日本色彩
部落格內容洞察儀表板
以視覺化儀表板方式追蹤文章成效、分享熱度與分類分布,協助創作者掌握內容表現。
蒙特卡羅估算 π
使用蒙特卡羅方法演示 π 值的估算過程
LLM
使用 LLM 模型進行聊天
活動圖生成器
一鍵創建活動的封面圖
Wagmi Card
一鍵創建 Wagmi 的封面圖
Facebook Quote
Facebook Quote
Music Macro Language (MML) Studio
用程式語法編寫旋律,用音符構築想像
Blurhash
一鍵創建 Blurhash
文字分類器
使用 MediaPipe TextClassifier 分類文字
前端工程師免費工具資源
前端工程師免費工具資源
後端工程師免費工具資源
後端工程師免費工具資源
全端工程師免費工具資源
全端工程師免費工具資源
Web3 工程師免費工具資源
Web3 工程師免費工具資源
紫微斗數排盤系統|結合 AI 的命盤性格與事業財務分析生成器
紫微斗數排盤工具,輸入生日與時辰,自動生成完整命盤分析提示(Prompt)。結合最專業紫微理論與 AI 助力,助你深入解析性格、事業、財務與人際課題。免費使用,適合命理師及紫微愛好者。
PixAI Prompt 組合器|快速打造可用於 AI 繪圖的語言拼圖
使用 PixAI 卻不會寫 prompt?這個工具幫你一鍵組裝角色、表情、風格語彙,輸出高品質繪圖提示語句(Prompt),可直接貼入 PixAI 使用。適合插畫師、創作者、AI 新手與 VTuber 角色開發者。