immer vs zustand vs redux vs mobx vs valtio vs recoil
React アプリケーションにおける状態管理ライブラリの選択
immerzustandreduxmobxvaltiorecoil類似パッケージ:

React アプリケーションにおける状態管理ライブラリの選択

immer、mobx、recoil、redux、valtio、zustand はすべて JavaScript/TypeScript エコシステムで広く使われる状態管理ソリューションであり、React アプリケーションを中心に、アプリケーションの状態を一貫性のある方法で管理・更新するための手段を提供します。これらのライブラリは、不変性(immutability)の扱い方、再レンダリングの最適化、API の設計思想、および開発体験(DX)において異なるアプローチを採用しています。たとえば redux は予測可能な単一ストアと明示的なアクションに基づく厳格なアーキテクチャを提供し、zustand はシンプルで軽量なフックベースの API を特徴とします。一方、immer は不変性を維持しながらミュータブルな記法でオブジェクトを操作できるユーティリティであり、他の状態管理ライブラリと組み合わせて使用されることが多いです。mobx と valtio はプロキシベースのリアクティブな状態管理を実現し、recoil はアトミックな状態単位と派生状態(selector)による柔軟なデータフローを提供します。

npmのダウンロードトレンド

3 年

GitHub Starsランキング

統計詳細

パッケージ
ダウンロード数
Stars
サイズ
Issues
公開日時
ライセンス
immer52,176,69028,994977 kB445日前MIT
zustand40,927,50258,87296 kB45日前MIT
redux35,215,79361,446290 kB153年前MIT
mobx3,914,68428,2164.88 MB193日前MIT
valtio1,642,53110,245101 kB66ヶ月前MIT
recoil437,30919,4112.21 MB3204年前MIT

React 状態管理ライブラリ徹底比較:Immer、MobX、Recoil、Redux、Valtio、Zustand

React アプリケーションの状態管理は、規模やチームの好みによってさまざまなアプローチがあります。ここでは、6つの主要なライブラリ — immer、mobx、recoil、redux、valtio、zustand — を、実際の開発シーンに即して比較します。各ライブラリの設計思想、使い方、再レンダリング挙動、そしてコード例を通じて、あなたのプロジェクトに最適な選択肢を見極めましょう。

🧠 基本的な役割と設計思想

まず、各ライブラリが「何を解決しようとしているか」を整理します。

  • immer:不変性を保ちつつ、ミュータブルな記法でオブジェクトを操作できるユーティリティ。単体では状態管理ライブラリではない。
  • mobx:プロキシと自動追跡で、リアクティブな状態管理を実現。
  • recoil:アトム(最小単位の状態)とセレクタ(派生状態)で構成される、細粒度な状態管理。
  • redux:単一ストア、純粋な reducer、明示的なアクションによる、予測可能な状態管理。
  • valtio:プロキシベースのリアクティブな状態を、極めてシンプルな API で提供。
  • zustand:グローバル状態をカスタムフックで提供し、Context API の罠を回避。

✍️ 状態の更新方法:ミュータブル vs イミュータブル vs リアクティブ

immer:イミュータブルだけど「ミュータブルに書ける」

immer は produce 関数を使って、イミュータブルな更新を「ミュータブルに見える」記法で書けます。

import { produce } from 'immer';

const nextState = produce(currentState, draft => {
  draft.user.name = 'Alice'; // 実際には新しいオブジェクトが作られる
});

これは Redux や Zustand と組み合わせて使われることが多いです。

mobx:完全にミュータブル、でも自動追跡

mobx では状態を直接変更できます。変更を検知して自動で再レンダリングされます。

import { makeAutoObservable } from 'mobx';

class Store {
  user = { name: 'Bob' };
  constructor() {
    makeAutoObservable(this);
  }
  updateName(name) {
    this.user.name = name; // 直接代入
  }
}

recoil:イミュータブル更新、ただし setter で抽象化

Recoil は set 関数で状態を更新します。古い値を引数に取る関数形式もサポート。

import { atom, useSetRecoilState } from 'recoil';

const userState = atom({ key: 'user', default: { name: 'Charlie' } });

function Component() {
  const setUser = useSetRecoilState(userState);
  const handleClick = () => {
    setUser(prev => ({ ...prev, name: 'David' }));
  };
}

redux:純粋関数でイミュータブル更新

Redux では reducer が純粋関数でなければならず、常に新しい状態を返す必要があります。

// reducer
const userReducer = (state = { name: 'Eve' }, action) => {
  if (action.type === 'UPDATE_NAME') {
    return { ...state, name: action.payload };
  }
  return state;
};

// dispatch
store.dispatch({ type: 'UPDATE_NAME', payload: 'Frank' });

valtio:プロキシで直接操作、でも安全に読み取り

Valtio では状態オブジェクトを直接変更できますが、React 側では useSnapshot で安全に参照します。

import { proxy, useSnapshot } from 'valtio';

const state = proxy({ user: { name: 'Grace' } });

function Component() {
  const snap = useSnapshot(state);
  const handleClick = () => {
    state.user.name = 'Helen'; // 直接変更
  };
  return <div>{snap.user.name}</div>;
}

zustand:setter でイミュータブル更新(ただし immer と組み合わせ可能)

Zustand は setter 関数で状態を更新します。デフォルトではイミュータブルですが、immer と組み合わせればミュータブル記法も使えます。

import { create } from 'zustand';

const useStore = create(set => ({
  user: { name: 'Ivy' },
  updateUser: (name) => set(state => ({
    user: { ...state.user, name }
  }))
}));

// コンポーネント内
const { user, updateUser } = useStore();
updateUser('Judy');

🔄 再レンダリングの最適化:どのコンポーネントが更新されるか?

immer:再レンダリング制御は委ね先次第

immer 自体は再レンダリングを制御しません。Redux や Zustand と組み合わせた場合、それらの仕組みに従います。

mobx:自動依存追跡で最小限の更新

MobX は observable へのアクセスを自動で追跡し、変更された observable に依存するコンポーネントのみを再レンダリングします。

import { observer } from 'mobx-react-lite';

const UserComponent = observer(() => {
  return <div>{store.user.name}</div>; // store.user.name に依存
});

recoil:アトム単位でのサブスクリプション

Recoil は、コンポーネントが subscribe したアトム(またはセレクタ)が変更されたときだけ再レンダリングされます。

const UserName = () => {
  const name = useRecoilValue(userState); // userState に依存
  return <div>{name}</div>;
};

redux:手動でメモ化が必要

Redux では、useSelector の結果が変わらない限り再レンダリングされませんが、深いオブジェクトの場合は shallowEqual や reselect でメモ化する必要があります。

import { useSelector } from 'react-redux';
import { shallowEqual } from 'react-redux';

const name = useSelector(state => state.user.name, shallowEqual);

valtio:プロキシのプロパティ単位で更新

Valtio は、useSnapshot がアクセスしたプロパティだけを監視し、そのプロパティが変更されたときだけ再レンダリングします。

const snap = useSnapshot(state);
return <div>{snap.user.name}</div>; // user.name だけ監視

zustand:セレクタで部分的な状態を購読

Zustand は useStore(selector) で特定のフィールドだけを購読でき、そのフィールドが変わったときだけ再レンダリングされます。

const name = useStore(state => state.user.name);

🧩 非同期処理と副作用の扱い

immer:非同期処理のサポートなし(委ね先次第)

mobx:アクション内で非同期処理可能

updateUserAsync = flow(function* (id) {
  const user = yield fetchUser(id);
  this.user = user;
});

recoil:セレクタで非同期データ取得

const userQuery = selector({
  key: 'userQuery',
  get: async ({ get }) => {
    const id = get(userIdState);
    const res = await fetch(`/api/user/${id}`);
    return res.json();
  }
});

redux:ミドルウェア(Thunk, Saga)で非同期処理

// Redux Thunk
const fetchUser = (id) => async (dispatch) => {
  const user = await api.getUser(id);
  dispatch({ type: 'SET_USER', payload: user });
};

valtio:非同期処理は通常の async/await で直接書ける

const fetchUser = async (id) => {
  const user = await fetch(`/api/user/${id}`).then(r => r.json());
  state.user = user;
};

zustand:非同期処理はアクション内で直接書ける

const useStore = create(set => ({
  fetchUser: async (id) => {
    const user = await fetch(`/api/user/${id}`).then(r => r.json());
    set({ user });
  }
}));

🛠️ 実際のコード例:ユーザープロフィールの更新

以下は、各ライブラリで「ユーザ名を更新する」機能を実装した最小コードです。

immer + zustand(よくある組み合わせ)

import { create } from 'zustand';
import { produce } from 'immer';

const useStore = create(set => ({
  user: { name: 'Alice' },
  updateName: (name) => set(produce(draft => {
    draft.user.name = name;
  }))
}));

mobx

import { makeAutoObservable } from 'mobx';

class UserStore {
  user = { name: 'Alice' };
  constructor() { makeAutoObservable(this); }
  updateName(name) { this.user.name = name; }
}
const store = new UserStore();

recoil

import { atom, useSetRecoilState } from 'recoil';

const userState = atom({ key: 'user', default: { name: 'Alice' } });

const useUpdateName = () => {
  const setUser = useSetRecoilState(userState);
  return (name) => setUser(prev => ({ ...prev, name }));
};

redux

// slice
const userSlice = createSlice({
  name: 'user',
  initialState: { name: 'Alice' },
  reducers: {
    updateName: (state, action) => {
      state.name = action.payload;
    }
  }
});

valtio

import { proxy } from 'valtio';

const state = proxy({ user: { name: 'Alice' } });

// 更新
state.user.name = 'Bob';

zustand

import { create } from 'zustand';

const useStore = create(set => ({
  user: { name: 'Alice' },
  updateName: (name) => set(state => ({
    user: { ...state.user, name }
  }))
}));

📊 まとめ:各ライブラリの特徴と向き不向き

ライブラリ学習コストボイラープレート再レンダリング最適化非同期対応向いているケース
immer低なし(補助ツール)なしなし不変性操作を簡潔にしたいとき
mobx中低⭐⭐⭐(自動)⭐⭐⭐複雑な状態で自然な書き方がしたい
recoil中中⭐⭐⭐(細粒度)⭐⭐⭐細かい状態単位で管理したい
redux高高⭐(手動)⭐⭐⭐(ミドルウェア)大規模・長期運用・厳格な管理が必要
valtio低極低⭐⭐⭐(プロパティ単位)⭐⭐小〜中規模で素早く実装したい
zustand低低⭐⭐(セレクタで部分購読)⭐⭐⭐多くのプロジェクトでバランスが良い

💡 最終的なアドバイス

  • シンプルさと速度が最優先 → zustand または valtio
  • 大規模でチーム開発、かつ厳格なトレーサビリティが必要 → redux(+ immer)
  • リアクティブで自然な JavaScript の書き方を重視 → mobx または valtio
  • 細かい状態単位と派生状態を活用したい → recoil
  • 既存の Redux/Zustand で不変性操作が面倒 → immer を追加

どのライブラリも「正解」はありません。あなたのチームのスキルセット、プロジェクトの規模、保守期間、そして「開発体験の好み」を元に、最もストレスなく開発を続けられるものを選んでください。

選び方: immer vs zustand vs redux vs mobx vs valtio vs recoil

  • immer:

    immer は単体で状態管理を行うライブラリではなく、不変性を保ちながら「ミュータブルに書ける」ようにするためのユーティリティです。Redux や Zustand など、手動で不変性を守る必要があるライブラリと組み合わせて使うと、コードの可読性と保守性が大幅に向上します。特に、深いネストを持つオブジェクトや配列を頻繁に更新するケースで有効です。ただし、プロキシやラッパーを使わない純粋な不変性操作が必要な場合は不要です。

  • zustand:

    zustand はグローバルな状態をカスタムフックとして提供する、非常にシンプルで軽量なライブラリです。Context API を使わずに状態を共有でき、ミドルウェアや永続化も容易に拡張できます。Redux のような厳格さは不要だが、グローバル状態が必要な中規模アプリケーションに最適です。また、immer と組み合わせることで不変性の煩雑さを軽減できます。学習コストが低く、初期設定も最小限なので、多くのプロジェクトで即戦力になります。

  • redux:

    redux は予測可能な単一ストア、純粋関数による reducer、そして明示的なアクションディスパッチを特徴とする、最も成熟した状態管理ライブラリです。時間旅行デバッグやミドルウェアによる副作用管理(例: Redux Thunk, Redux Saga)など、強力な開発ツールとエコシステムがあります。大規模チームや長期運用が想定されるプロジェクトで、状態の変更を厳密に追跡・テストしたい場合に適しています。ただし、ボイラープレートが多く、学習コストが高い点は考慮すべきです。

  • mobx:

    mobx はプロキシと装飾子(decorator)または関数 API を使って、リアクティブな状態管理を実現します。自動的な依存追跡により、状態の一部が変更されたときに必要なコンポーネントだけが再レンダリングされる仕組みを持ちます。大規模で複雑な状態を持つアプリケーションで、開発者が「何が変わるか」を気にせず自然な JavaScript の書き方で状態を操作したい場合に最適です。ただし、React の strict mode や concurrent features との互換性には注意が必要です。

  • valtio:

    valtio はプロキシを使ってリアクティブな状態をシンプルに扱えるライブラリです。状態オブジェクトを直接操作でき、その変更を useSnapshot で安全に React コンポーネント内で参照できます。Zustand よりもさらに少ないコードでリアクティブな状態管理が可能で、小〜中規模のアプリケーションや素早くプロトタイプを作りたい場合に有効です。ただし、深いネストや複雑な状態ロジックではデバッグが難しくなる可能性があります。

  • recoil:

    recoil は Facebook が開発した、アトム(atom)とセレクタ(selector)を基盤とする状態管理ライブラリです。状態を細かい単位(アトム)で定義し、それらを組み合わせて派生状態(セレクタ)を作れます。非同期データ取得やメモ化も自然に統合できます。コンポーネント間で状態を共有しつつ、再レンダリングを最小限に抑えたい中〜大規模アプリケーションに向いています。ただし、プロジェクトが Recoil のメンテナンス状況(2024年時点で公式に「安定版」として推奨されているが、新機能の追加は限定的)を許容できる必要があります。

immer のREADME

Immer

npm Build Status Coverage Status code style: prettier OpenCollective OpenCollective Gitpod Ready-to-Code

Create the next immutable state tree by simply modifying the current tree

Winner of the "Breakthrough of the year" React open source award and "Most impactful contribution" JavaScript open source award in 2019

Contribute using one-click online setup

You can use Gitpod (a free online VSCode like IDE) for contributing online. With a single click it will launch a workspace and automatically:

  • clone the immer repo.
  • install the dependencies.
  • run yarn run start.

so that you can start coding straight away.

Open in Gitpod

Documentation

The documentation of this package is hosted at https://immerjs.github.io/immer/

Support

Did Immer make a difference to your project? Join the open collective at https://opencollective.com/immer!

Release notes

https://github.com/immerjs/immer/releases