immer vs zustand vs redux vs xstate vs mobx vs valtio vs react-query vs recoil
Reactアプリケーションにおける状態管理と副作用処理のライブラリ選定ガイド
immerzustandreduxxstatemobxvaltioreact-queryrecoil유사 패키지:

Reactアプリケーションにおける状態管理と副作用処理のライブラリ選定ガイド

immer、mobx、react-query、recoil、redux、valtio、xstate、zustand はすべて、Reactベースのフロントエンドアプリケーションで状態(state)や非同期データ、副作用を扱うためのツールです。これらのパッケージは目的や設計思想が大きく異なります。たとえば redux や zustand は主にグローバルなアプリケーション状態を管理し、react-query はサーバーから取得する非同期データのキャッシュや再取得を担います。immer はイミュータブルな更新を簡単にするユーティリティであり、xstate はステートマシンによる複雑なワークフロー制御を実現します。開発者はアプリの規模、チームのスキル、状態の性質(ローカルかグローバルか、同期か非同期か)に応じて適切なツールを選ぶ必要があります。

npm 다운로드 트렌드

3 년

GitHub Stars 순위

통계 세부사항

패키지
다운로드
Stars
크기
Issues
발행일
라이선스
immer90,253,48829,004977 kB4614日前MIT
zustand89,687,62358,89696 kB514日前MIT
redux63,508,39761,441290 kB173年前MIT
xstate8,468,87130,4052.31 MB13018日前MIT
mobx4,662,23728,2194.89 MB184日前MIT
valtio3,152,84510,244101 kB66ヶ月前MIT
react-query2,050,36050,5252.26 MB1124年前MIT
recoil580,06119,4072.21 MB3204年前MIT

React状態管理ライブラリ徹底比較:immer, mobx, react-query, recoil, redux, valtio, xstate, zustand

現代のReactアプリケーションでは、「状態」と一口に言っても、ローカルUI状態、グローバルアプリケーション状態、サーバーからの非同期データなど、さまざまな種類があります。この記事では、代表的な8つのnpmパッケージがそれぞれどのような問題を解決し、どのように使い分けるべきかを、実際のコードを交えて解説します。

🧠 状態の種類とライブラリの役割

まず、各ライブラリが主に扱う「状態の種類」を整理しましょう。

  • UI状態(ローカル or グローバル): モーダルの表示/非表示、フォーム入力値、テーマ設定など
  • サーバー状態: APIから取得したユーザーデータ、投稿リスト、検索結果など
  • ワークフロー状態: フォームのステップ進行、ゲームのシーン遷移、決済フローなど
ライブラリ主な用途
immerイミュータブル更新の簡略化(他の状態管理と併用)
mobxリアクティブなグローバルUI状態管理
react-queryサーバー状態の取得・キャッシュ・同期
recoilアトミックなグローバルUI状態と派生状態
redux予測可能なグローバルUI状態管理
valtioProxyベースの軽量グローバルUI状態管理
xstateステートマシンによるワークフロー制御
zustandHooksベースのシンプルなグローバルUI状態管理

🔁 イミュータブル更新:immer vs 他

多くの状態管理ライブラリはイミュータブルな更新を前提としますが、immer はそれを簡単にします。

immer

import { produce } from 'immer';

const nextState = produce(baseState, draft => {
  draft.user.name = 'Alice';
  draft.posts.push({ id: 3, title: 'New Post' });
});

immer は単体では状態を保持しません。通常はReduxのReducer内などで使います。

Redux(immerなし)

// 非常に冗長
const reducer = (state, action) => {
  if (action.type === 'UPDATE_USER') {
    return {
      ...state,
      user: {
        ...state.user,
        name: action.payload.name
      }
    };
  }
};

Zustand(immer併用可能)

import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';

const useStore = create(
  immer(set => ({
    user: { name: 'Bob' },
    updateUser: name => set(state => {
      state.user.name = name; // immerによりミュータブル記法可
    })
  }))
);

💡 immer は「更新ロジックの書きやすさ」に特化したユーティリティであり、状態管理ライブラリそのものではありません。


🌐 サーバー状態管理:react-query の特異性

react-query は唯一、サーバー状態専用のライブラリです。他の7つは主にUI状態を扱います。

react-query

import { useQuery } from 'react-query';

function UserProfile({ userId }) {
  const { data, isLoading, error } = useQuery(
    ['user', userId],
    () => fetchUser(userId)
  );

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error!</div>;
  return <div>{data.name}</div>;
}

自動で以下を処理します:

  • ローディング/エラー状態
  • キャッシュと自動再取得
  • バックグラウンド更新
  • ページ間でのデータ共有

他のライブラリ(例:Redux)で同じことをすると、大量のボイラープレートが必要です:

Redux(サーバー状態管理)

// actions
const fetchUserRequest = () => ({ type: 'FETCH_USER_REQUEST' });
const fetchUserSuccess = user => ({ type: 'FETCH_USER_SUCCESS', payload: user });
const fetchUserFailure = error => ({ type: 'FETCH_USER_FAILURE', payload: error });

// thunk
const fetchUser = userId => async dispatch => {
  dispatch(fetchUserRequest());
  try {
    const user = await api.getUser(userId);
    dispatch(fetchUserSuccess(user));
  } catch (error) {
    dispatch(fetchUserFailure(error));
  }
};

// component
const UserProfile = ({ userId }) => {
  const { user, loading, error } = useSelector(state => state.user);
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(fetchUser(userId));
  }, [userId]);

  // ... ローディング/エラーハンドリング
};

💡 サーバー状態には react-query(または SWR)、UI状態には他のライブラリを使うのが現代的なベストプラクティスです。


🧩 グローバルUI状態管理:redux, zustand, recoil, mobx, valtio

これら5つは、アプリ全体で共有するUI状態を管理しますが、アプローチが異なります。

Redux:明示的で予測可能なフロー

// store
const store = createStore(reducer, applyMiddleware(thunk));

// component
const Counter = () => {
  const count = useSelector(state => state.count);
  const dispatch = useDispatch();
  return (
    <button onClick={() => dispatch({ type: 'INCREMENT' })}>
      {count}
    </button>
  );
};
  • ✅ 時間旅行デバッグ、DevTools対応
  • ❌ ボイラープレートが多い

Zustand:Hooks風のシンプルAPI

const useStore = create(set => ({
  count: 0,
  increment: () => set(state => ({ count: state.count + 1 }))
}));

const Counter = () => {
  const { count, increment } = useStore();
  return <button onClick={increment}>{count}</button>;
};
  • ✅ 最小限のコードで開始可能
  • ✅ ミドルウェアで拡張可能

Recoil:アトミックな状態と派生状態

const countState = atom({ key: 'count', default: 0 });
const doubledCountState = selector({
  key: 'doubledCount',
  get: ({ get }) => get(countState) * 2
});

const Counter = () => {
  const [count, setCount] = useRecoilState(countState);
  const doubled = useRecoilValue(doubledCountState);
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      <div>Doubled: {doubled}</div>
    </div>
  );
};
  • ✅ 計算済み状態(selectors)が自然
  • ❓ 実験的段階のため長期サポートに注意

MobX:リアクティブなオブジェクト

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

class CounterStore {
  count = 0;
  constructor() { makeAutoObservable(this); }
  increment = () => { this.count++; }
}

const store = new CounterStore();

const Counter = observer(() => (
  <button onClick={store.increment}>{store.count}</button>
));
  • ✅ 自動的な依存追跡で最適な再レンダリング
  • ❌ デバッグが難しい場合あり

Valtio:Proxyによる軽量リアクティブ

import { proxy, useSnapshot } from 'valtio';

const state = proxy({ count: 0 });

const Counter = () => {
  const snapshot = useSnapshot(state);
  return (
    <button onClick={() => ++state.count}>
      {snapshot.count}
    </button>
  );
};
  • ✅ 普通のオブジェクト操作でリアクティブ更新
  • ✅ 学習コストが極めて低い

🎭 複雑なワークフロー:xstate の出番

マルチステップフォームやゲームロジックなど、明確な状態遷移がある場合は xstate が最適です。

xstate

import { createMachine, interpret } from 'xstate';

const formMachine = createMachine({
  id: 'form',
  initial: 'step1',
  states: {
    step1: { on: { NEXT: 'step2' } },
    step2: { on: { PREV: 'step1', SUBMIT: 'submitting' } },
    submitting: { invoke: { src: 'submitForm', onDone: 'success' } },
    success: { type: 'final' }
  }
});

// Reactコンポーネントで使用
import { useMachine } from '@xstate/react';

const Form = () => {
  const [state, send] = useMachine(formMachine);

  if (state.matches('step1')) {
    return <Step1 onNext={() => send('NEXT')} />;
  }
  if (state.matches('step2')) {
    return <Step2 onPrev={() => send('PREV')} onSubmit={() => send('SUBMIT')} />;
  }
  // ...
};
  • ✅ 状態遷移図で可視化可能
  • ✅ 不正な遷移を防げる
  • ❌ 単純な状態管理には不向き

📊 選定フローチャート:どのライブラリを選ぶべきか?

  1. サーバーからデータを取得する必要があるか?

    • はい → react-query(または SWR)
    • いいえ → 次へ
  2. 複雑なワークフローやステートマシンが必要か?

    • はい → xstate
    • いいえ → 次へ
  3. 既存の状態管理でイミュータブル更新が面倒か?

    • はい → immer を併用
    • いいえ → 次へ
  4. グローバルUI状態をどう管理するか?

    • ボイラープレートを避け、シンプルに → zustand または valtio
    • 計算済み状態を多用する → recoil
    • リアクティブなクラスベース設計を好む → mobx
    • 予測可能性とDevToolsを重視 → redux

💡 実践的なアドバイス

  • 新規プロジェクトでは:zustand(UI状態) + react-query(サーバー状態)の組み合わせがバランスが良く、学習コストも低いです。
  • 大規模チーム・長期運用:redux の予測可能性とエコシステムが有利ですが、zustand も十分に成熟しています。
  • プロトタイピングや小規模アプリ:valtio は数行でグローバル状態を実装でき、非常に高速に開発を進められます。
  • 複雑なユーザージャーニー:xstate を早期に導入することで、後々のバグ修正コストを大幅に削減できます。

🔄 まとめ:各ライブラリの本質

ライブラリコアアイデア向いているケース
immer「ミュータブルに書くがイミュータブルに振る舞う」既存のイミュータブル更新を簡略化したいとき
mobx自動的な依存追跡によるリアクティブ更新クラスベース設計を好み、パフォーマンスを重視するとき
react-queryサーバー状態のキャッシュと自動同期APIデータを扱うほぼすべての現代的Reactアプリ
recoilアトミックな状態と派生状態計算済み状態を多用する中〜大規模アプリ
redux単一の真実源と予測可能な状態変更大規模チームや厳格なデバッグが必要なプロジェクト
valtioProxyによる軽量リアクティブ状態小〜中規模アプリや迅速な開発を重視するとき
xstateステートマシンによる明確な状態遷移複雑なワークフローやユーザージャーニー
zustandHooks風の最小限API多くのケースでバランスの取れた選択肢

最終的には、アプリの要件とチームの好みに合わせて選ぶことが最も重要です。どのライブラリも成熟しており、適切に使えば高品質なアプリケーションを構築できます。

선택 방법: immer vs zustand vs redux vs xstate vs mobx vs valtio vs react-query vs recoil

  • immer:

    immer は既存の状態管理ライブラリ(Reduxなど)と組み合わせて、イミュータブルな更新を直感的に記述したい場合に最適です。特に、深いネストを持つオブジェクトを扱う際に、スプレッド演算子の代わりに「ミュータブルに書くがイミュータブルに振る舞う」コードを書ける点が大きな利点です。ただし、単体では状態の保持やコンポーネントへの通知機能を持たないため、他の状態管理システムとの併用が前提になります。

  • zustand:

    zustand は最小限のAPIでグローバル状態を管理したい場合に最適です。Reduxのようなボイラープレートがなく、Hooksベースで直感的に使用でき、ミドルウェアも簡単に拡張できます。特に、中規模以下のアプリで迅速に開発を進めたいチームや、Reduxの複雑さを避けたいケースに強く推奨されます。

  • redux:

    redux は予測可能な状態管理と強力な開発者ツール(DevTools)、ミドルウェアエコシステムを求める大規模アプリケーションに適しています。特に、時間旅行デバッグや厳密な状態変更ルールが必要なチームでは依然として有力な選択肢です。ただし、ボイラープレートが多く、シンプルなアプリでは過剰な設計になる可能性があります。

  • xstate:

    xstate は明確なステートマシンや状態遷移図で表現できる複雑なワークフローやユーザージャーニー(例:マルチステップフォーム、ゲームロジック)を実装したい場合に選ぶべきです。視覚化ツールとの連携により、バグの早期発見や仕様の明確化が可能です。ただし、シンプルな状態管理にはオーバーキルとなるため、用途を絞って使うことが重要です。

  • mobx:

    mobx はリアクティブなプログラミングモデルを好むチームや、クラスベースの状態構造を自然に扱いたい場合に向いています。自動的な依存追跡により、必要なコンポーネントだけが再レンダリングされるため、パフォーマンスが良好です。ただし、デバッグが難しくなる可能性があるため、TypeScriptとの併用と厳格なモード(useStrict)の活用が推奨されます。

  • valtio:

    valtio はProxyベースのリアクティブな状態管理を極めて軽量に実現したい場合に最適です。状態を普通のオブジェクトとして扱いながら、自動的にコンポーネントを更新できるため、学習コストが非常に低いです。小〜中規模のアプリやプロトタイピングに強く、Reduxのような明示的なアクションやReducerの記述が不要です。

  • react-query:

    react-query はサーバーからのデータ取得・キャッシュ・同期に特化しており、UI状態とは分離された「サーバー状態」を扱う必要があるアプリケーションで選ぶべきです。自動的なバックグラウンド更新、無効化、再試行などの高度な機能を提供し、手動でのローディング/エラー状態管理を不要にします。ただし、ローカルUI状態の管理には向いていません。

  • recoil:

    recoil はアトミックな状態単位で柔軟な依存関係を構築したい場合に有効です。特に、計算済みの派生状態(selectors)を簡単に作成でき、コンポーネント間で状態を共有しつつも最適化された再レンダリングを実現します。ただし、まだAPIが安定していない可能性があり、大規模プロジェクトでは長期的なメンテナンスコストを考慮する必要があります。

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