Immutable State を簡単に実現:Immer を理解する
JavaScript でネストされた状態を変更せずに更新するのは面倒です。すべてのレベルでオブジェクトをスプレッドし、どの参照が変更されたかを追跡し、誤って何かを変更していないことを願うしかありません。シンプルなネストされた更新でも、慎重なコピーロジックを5行書く必要があるかもしれません。
Immer は、この問題を異なるアプローチで解決します。変更のように見えるコードを書くだけで、自動的にイミュータブルな状態を生成します。この記事では、Immer のプロキシベースのイミュータビリティがどのように機能するか、なぜ Redux Toolkit が内部で使用しているか、そして採用する前に知っておくべき実用的な注意点について説明します。
重要なポイント
- Immer は
produce関数とプロキシベースのドラフトを通じて、変更スタイルのコードを書きながらイミュータブルな状態を生成できます - 構造共有により、変更されたブランチのみが新しい参照を取得し、React と Redux の再レンダリングチェックのパフォーマンスが維持されます
- Redux Toolkit は内部で Immer を使用しているため、
createSliceのリデューサーは追加のインポートなしで自動的にイミュータビリティを処理します - よくある落とし穴に注意:ドラフト自体を再代入しない、ドラフトの変更と戻り値を混在させない、クラスインスタンスには慎重に対応する
Immer がイミュータブルな状態を生成する仕組み
Immer のコア API は produce 関数です。現在の状態と「レシピ」関数を渡します。そのレシピ内で、元の状態をラップした draft(ドラフト)—プロキシを受け取ります。通常の JavaScript の変更を使用してドラフトを変更します。レシピが終了すると、Immer は変更を反映した新しいイミュータブルな状態を生成します。
import { produce } from "immer"
const baseState = {
user: { name: "Alice", settings: { theme: "dark" } }
}
const nextState = produce(baseState, draft => {
draft.user.settings.theme = "light"
})
// baseState.user.settings.theme は依然として "dark"
// nextState.user.settings.theme は "light"
メンタルモデルは単純です。変更しているふりをすれば、Immer が裏でイミュータブルな更新を処理してくれます。
プロキシベースのイミュータビリティと構造共有
Immer は JavaScript の Proxy オブジェクトを使用して、ドラフトへの読み取りと書き込みをインターセプトします。ネストされたプロパティにアクセスすると、Immer はそのパスのプロキシを遅延的に作成します。プロパティに書き込むと、Immer はそのノード(とその祖先)を変更済みとしてマークし、必要な場所でのみシャローコピーを作成します。
このコピーオンライト方式により、構造共有が可能になります。状態ツリーの変更されていない部分は、同じオブジェクト参照のままです。変更されたブランチのみが新しい参照を取得します。これは React と Redux にとって重要です。なぜなら、参照の等価性チェックがコンポーネントの再レンダリングを決定するからです。
最新の Immer(v10+)はネイティブの Proxy サポートを必要とします—ES5 フォールバックはありません。これは現在のブラウザと Node.js では問題ありませんが、特殊な環境をターゲットにする場合は注意が必要です。
Redux Toolkit の Immer 統合
Redux Toolkit を使用している場合、すでに Immer を使用しています。RTK の createSlice は、リデューサーロジックを自動的に produce でラップします。ケースリデューサーで「変更する」コードを書けば、RTK がイミュータビリティを処理します。
import { createSlice } from "@reduxjs/toolkit"
const todosSlice = createSlice({
name: "todos",
initialState: [],
reducers: {
addTodo: (state, action) => {
state.push(action.payload) // 変更のように見えますが、安全です
},
toggleTodo: (state, action) => {
const todo = state.find(t => t.id === action.payload)
if (todo) todo.completed = !todo.completed
}
}
})
この Redux Toolkit の Immer 統合により、バニラ Redux のリデューサーを冗長にしていたスプレッド演算子のボイラープレートが不要になります。produce を個別にインポートする必要はありません—RTK が自動的に設定してくれます。
Discover how at OpenReplay.com.
自動フリーズと反復動作
Immer はデフォルトで生成された状態を自動的にフリーズします(Object.freeze を使用)。これにより開発中の偶発的な変更を検出できますが、オーバーヘッドが追加されます。プロファイリングで重要であることが示された場合、本番環境では設定により無効化できます。
import { setAutoFreeze } from "immer"
setAutoFreeze(false) // パフォーマンスのため本番環境では無効化
最新の Immer はパフォーマンスのためデフォルトで緩い反復を使用します。ドラフトでは列挙可能な文字列キーのみが反復されます。これは通常、アプリケーション状態に必要なものですが、明示的に厳密な反復を有効にしない限り、シンボルキーと列挙不可能なプロパティはスキップされます。これは主にエッジケースや低レベルのデータ操作で重要になります。
JavaScript イミュータブル更新の実用的な注意点
Immer に慣れていない開発者がつまずくいくつかの落とし穴があります。
ドラフト自体を再代入しないでください。 draft.property の変更は機能します。draft = newValue の再代入は、ローカルパラメータのバインディングを変更するだけなので、役に立ちません。
戻り値が重要です。 レシピが値を返す場合、Immer は変更されたドラフトの代わりにその値を新しい状態として使用します。undefined を返すことは何も返さないことと同じように扱われますが、ドラフトの変更と明示的な戻り値を混在させることはバグの一般的な原因です—どちらか一方のアプローチを使用してください。
クラスとエキゾチックなオブジェクトには注意が必要です。 Immer はプレーンオブジェクト、配列、Map、Set で最も効果的に機能します。クラスインスタンスは自動的に正しくプロキシ化されません。それらをイミュータブルとしてマークするか、個別に処理する必要があるかもしれません(公式の落とし穴ドキュメントに概説されています)。
パフォーマンスは無料ではありません。 Immer は典型的な UI 状態—フォーム、リスト、ユーザー設定—に適しています。フレームごとに数千のアイテムを処理するホットループの場合、Immer がオーバーヘッドを追加しないと仮定する前に測定してください。プロキシの仕組みにはコストがあります。
ツリー状の状態が最適です。 Immer は状態がツリーであることを前提としています。循環参照やブランチ間で共有されるオブジェクト参照は、予期しない結果を生じる可能性があります。
まとめ
Immer は、適度に複雑でネストされた状態更新に優れています—これは React コンポーネントと Redux リデューサーで遭遇するものです。ボイラープレートを排除し、偶発的な変更を検出し、Redux Toolkit とシームレスに統合されます。
シンプルなフラットな状態の場合、ネイティブのスプレッド演算子で十分です。パフォーマンスが重要なデータ処理の場合は、まずベンチマークを取ってください。しかし、典型的なフロントエンド状態管理では、Immer のプロキシベースのアプローチが開発者体験と正確性の最良のバランスを提供します。
よくある質問
はい。状態更新を produce で直接ラップするか、use-immer パッケージの useImmer フックを使用できます。これにより、Redux Toolkit がグローバル状態に提供するのと同じ変更スタイルの構文をローカルコンポーネント状態に使用できます。
Immer は優れた TypeScript サポートを持っています。produce 関数はベース状態から型を自動的に推論し、ドラフトオブジェクトは適切な型付けを維持します。変更スタイルのコードを書きながら、完全な自動補完と型チェックが得られます。
Immer は Map と Set をネイティブにサポートしています。最新バージョンでは、追加の設定なしに、ドラフト上で set、delete、add などの標準的な Map と Set のメソッドを直接使用できます。
プロファイリングでパフォーマンスの問題が示された場合のみです。自動フリーズは、偶発的な変更に対してエラーをスローすることでバグの検出に役立ちます。ほとんどのアプリケーションではオーバーヘッドはわずかですが、高頻度の更新では setAutoFreeze false で無効化することで恩恵を受ける可能性があります。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.