immer、mobx、react-query、recoil、redux、valtio、xstate、zustand はすべて、Reactベースのフロントエンドアプリケーションで状態(state)や非同期データ、副作用を扱うためのツールです。これらのパッケージは目的や設計思想が大きく異なります。たとえば redux や zustand は主にグローバルなアプリケーション状態を管理し、react-query はサーバーから取得する非同期データのキャッシュや再取得を担います。immer はイミュータブルな更新を簡単にするユーティリティであり、xstate はステートマシンによる複雑なワークフロー制御を実現します。開発者はアプリの規模、チームのスキル、状態の性質(ローカルかグローバルか、同期か非同期か)に応じて適切なツールを選ぶ必要があります。
現代のReactアプリケーションでは、「状態」と一口に言っても、ローカルUI状態、グローバルアプリケーション状態、サーバーからの非同期データなど、さまざまな種類があります。この記事では、代表的な8つのnpmパッケージがそれぞれどのような問題を解決し、どのように使い分けるべきかを、実際のコードを交えて解説します。
まず、各ライブラリが主に扱う「状態の種類」を整理しましょう。
| ライブラリ | 主な用途 |
|---|---|
immer | イミュータブル更新の簡略化(他の状態管理と併用) |
mobx | リアクティブなグローバルUI状態管理 |
react-query | サーバー状態の取得・キャッシュ・同期 |
recoil | アトミックなグローバルUI状態と派生状態 |
redux | 予測可能なグローバルUI状態管理 |
valtio | Proxyベースの軽量グローバルUI状態管理 |
xstate | ステートマシンによるワークフロー制御 |
zustand | HooksベースのシンプルなグローバルUI状態管理 |
多くの状態管理ライブラリはイミュータブルな更新を前提としますが、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内などで使います。
// 非常に冗長
const reducer = (state, action) => {
if (action.type === 'UPDATE_USER') {
return {
...state,
user: {
...state.user,
name: action.payload.name
}
};
}
};
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 は唯一、サーバー状態専用のライブラリです。他の7つは主にUI状態を扱います。
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)で同じことをすると、大量のボイラープレートが必要です:
// 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状態には他のライブラリを使うのが現代的なベストプラクティスです。
これら5つは、アプリ全体で共有するUI状態を管理しますが、アプローチが異なります。
// 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>
);
};
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>;
};
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>
);
};
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>
));
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 が最適です。
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')} />;
}
// ...
};
サーバーからデータを取得する必要があるか?
react-query(または SWR)複雑なワークフローやステートマシンが必要か?
xstate既存の状態管理でイミュータブル更新が面倒か?
immer を併用グローバルUI状態をどう管理するか?
zustand または valtiorecoilmobxreduxzustand(UI状態) + react-query(サーバー状態)の組み合わせがバランスが良く、学習コストも低いです。redux の予測可能性とエコシステムが有利ですが、zustand も十分に成熟しています。valtio は数行でグローバル状態を実装でき、非常に高速に開発を進められます。xstate を早期に導入することで、後々のバグ修正コストを大幅に削減できます。| ライブラリ | コアアイデア | 向いているケース |
|---|---|---|
immer | 「ミュータブルに書くがイミュータブルに振る舞う」 | 既存のイミュータブル更新を簡略化したいとき |
mobx | 自動的な依存追跡によるリアクティブ更新 | クラスベース設計を好み、パフォーマンスを重視するとき |
react-query | サーバー状態のキャッシュと自動同期 | APIデータを扱うほぼすべての現代的Reactアプリ |
recoil | アトミックな状態と派生状態 | 計算済み状態を多用する中〜大規模アプリ |
redux | 単一の真実源と予測可能な状態変更 | 大規模チームや厳格なデバッグが必要なプロジェクト |
valtio | Proxyによる軽量リアクティブ状態 | 小〜中規模アプリや迅速な開発を重視するとき |
xstate | ステートマシンによる明確な状態遷移 | 複雑なワークフローやユーザージャーニー |
zustand | Hooks風の最小限API | 多くのケースでバランスの取れた選択肢 |
最終的には、アプリの要件とチームの好みに合わせて選ぶことが最も重要です。どのライブラリも成熟しており、適切に使えば高品質なアプリケーションを構築できます。
immer は既存の状態管理ライブラリ(Reduxなど)と組み合わせて、イミュータブルな更新を直感的に記述したい場合に最適です。特に、深いネストを持つオブジェクトを扱う際に、スプレッド演算子の代わりに「ミュータブルに書くがイミュータブルに振る舞う」コードを書ける点が大きな利点です。ただし、単体では状態の保持やコンポーネントへの通知機能を持たないため、他の状態管理システムとの併用が前提になります。
zustand は最小限のAPIでグローバル状態を管理したい場合に最適です。Reduxのようなボイラープレートがなく、Hooksベースで直感的に使用でき、ミドルウェアも簡単に拡張できます。特に、中規模以下のアプリで迅速に開発を進めたいチームや、Reduxの複雑さを避けたいケースに強く推奨されます。
redux は予測可能な状態管理と強力な開発者ツール(DevTools)、ミドルウェアエコシステムを求める大規模アプリケーションに適しています。特に、時間旅行デバッグや厳密な状態変更ルールが必要なチームでは依然として有力な選択肢です。ただし、ボイラープレートが多く、シンプルなアプリでは過剰な設計になる可能性があります。
xstate は明確なステートマシンや状態遷移図で表現できる複雑なワークフローやユーザージャーニー(例:マルチステップフォーム、ゲームロジック)を実装したい場合に選ぶべきです。視覚化ツールとの連携により、バグの早期発見や仕様の明確化が可能です。ただし、シンプルな状態管理にはオーバーキルとなるため、用途を絞って使うことが重要です。
mobx はリアクティブなプログラミングモデルを好むチームや、クラスベースの状態構造を自然に扱いたい場合に向いています。自動的な依存追跡により、必要なコンポーネントだけが再レンダリングされるため、パフォーマンスが良好です。ただし、デバッグが難しくなる可能性があるため、TypeScriptとの併用と厳格なモード(useStrict)の活用が推奨されます。
valtio はProxyベースのリアクティブな状態管理を極めて軽量に実現したい場合に最適です。状態を普通のオブジェクトとして扱いながら、自動的にコンポーネントを更新できるため、学習コストが非常に低いです。小〜中規模のアプリやプロトタイピングに強く、Reduxのような明示的なアクションやReducerの記述が不要です。
react-query はサーバーからのデータ取得・キャッシュ・同期に特化しており、UI状態とは分離された「サーバー状態」を扱う必要があるアプリケーションで選ぶべきです。自動的なバックグラウンド更新、無効化、再試行などの高度な機能を提供し、手動でのローディング/エラー状態管理を不要にします。ただし、ローカルUI状態の管理には向いていません。
recoil はアトミックな状態単位で柔軟な依存関係を構築したい場合に有効です。特に、計算済みの派生状態(selectors)を簡単に作成でき、コンポーネント間で状態を共有しつつも最適化された再レンダリングを実現します。ただし、まだAPIが安定していない可能性があり、大規模プロジェクトでは長期的なメンテナンスコストを考慮する必要があります。
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
You can use Gitpod (a free online VSCode like IDE) for contributing online. With a single click it will launch a workspace and automatically:
yarn run start.so that you can start coding straight away.
The documentation of this package is hosted at https://immerjs.github.io/immer/
Did Immer make a difference to your project? Join the open collective at https://opencollective.com/immer!