はじめに
ReactやReduxでstateを更新するとき、ネストしたオブジェクトの一部だけを変更したいのに、
スプレッド構文 ... を何段にも重ねて書いた経験はありませんか。
setState({
...state,
user: {
...state.user,
address: {
...state.user.address,
city: "Tokyo",
},
},
});
たった1つのプロパティを変えたいだけなのに、コードは読みにくく、
どこかで ... を書き忘れると簡単にバグります。Immerはこの問題を、
「まるでミュータブルに書いているのに、実際は新しいイミュータブルなstateが生成される」
という仕組みで解決してくれるライブラリです。
Immerとは
Immer(ドイツ語で「常に」の意)は、不変性(イミュータブル)を保ったまま
現在のstateを直接書き換えるような感覚でコーディングできるライブラリです。
内部ではJavaScriptの Proxy を使い、変更操作をすべて記録した上で、
最終的に元のオブジェクトを一切壊さない新しいオブジェクトを生成します。
Redux Toolkitの内部でも標準採用されており、React・Vue・Vanilla JSを問わず利用できます。
主な特徴
- 直感的な書き方 -
state.foo = "bar"のようにミュータブルな構文でstateを更新できる - 深いネストにも強い - スプレッド構文を重ねる必要がなく、ネストが深くても1行で済む
- 参照の共有によるパフォーマンス - 変更されていない部分は元のオブジェクトの参照をそのまま再利用する
- Structural Sharing - 変更前後で差分だけが新しいオブジェクトになるため、Reactの再レンダリング最適化とも相性が良い
インストール
npm install immer
yarn add immer
pnpm add immer
基本的な使い方
中心となるのは produce 関数です。第1引数に元のstate、第2引数に
「draft(下書き)」を直接書き換えるコールバックを渡すと、新しいstateが返ってきます。
draft.todos[0].done = true のように直接代入しているにもかかわらず、
baseState は一切変更されず、nextState という新しいオブジェクトが作られている点がポイントです。
実践的なユースケース
Reactのstateフックと組み合わせる
useState でネストしたオブジェクトを管理する場合、更新関数の中で produce を使うと、
スプレッド構文を書かずに済みます。フォームの一部フィールドだけを更新するようなケースで特に効果を発揮します。
setProfile(produce((draft) => { ... })) のように、produce へ渡すレシピ関数だけを
setState のセッター関数として渡す「カリー化」の書き方も可能です。
配列の複雑な操作をシンプルにする
配列内の特定要素の削除・並び替え・条件付き更新は、通常だと filter や map を
組み合わせる必要があります。Immerなら splice や push といった配列のミュータブルメソッドを
そのまま使えるため、直感的に書けます。
find や splice は本来配列を破壊的に変更するメソッドですが、
draft上で使う分にはimmutableな結果として反映されるため、安心して使えます。
Reducerパターンで状態遷移を管理する
Redux風の switch 文によるreducerも、Immerで書くと各caseがそのまま
「stateをどう書き換えるか」を表現するだけのシンプルなコードになります。
produce にレシピ関数だけを渡すと、(state, action) => newState という
reducerとしてそのまま使える関数が返ってきます。Redux Toolkitの createSlice が
内部で行っているのもこの仕組みです。
まとめ
Immerを使うことで、ネストしたstateの更新はスプレッド構文の羅列から解放され、 「draftを直接書き換える」という直感的なコードに変わります。それでいて実際には 不変性が保たれており、変更されていない部分は参照が共有されるためパフォーマンス面でも有利です。 React・Redux・Vueなど、どのstate管理と組み合わせても効果を発揮するので、 ネストしたstateの更新でコードが読みにくくなっていると感じたら、ぜひ導入を検討してみてください。