effector、mobx、react-query、recoil、redux、redux-saga、redux-thunk、xstate、zustand は、すべてReactベースのフロントエンドアプリケーションで状態(state)や非同期処理を扱うためのライブラリです。これらのツールは、UIの状態、サーバーとのデータ同期、複雑なワークフローのモデリングなど、さまざまな課題に対応しています。たとえば redux は予測可能な単一ストアのアーキテクチャを提供し、react-query はサーバー状態の取得・キャッシュに特化しています。一方、xstate は状態遷移を明示的にモデリングする有限状態機械(FSM)を実装し、zustand や recoil はより軽量で直感的なAPIを提供します。この比較では、各ライブラリの設計思想、使いどころ、実装例を通じて、適切な選択を支援します。
現代のReactアプリケーションでは、ローカルUI状態、サーバー状態、複雑な非同期ワークフローをどう扱うかが重要な設計課題です。ここでは、代表的な9つのライブラリ(effector、mobx、react-query、recoil、redux、redux-saga、redux-thunk、xstate、zustand)を、実際のコード例とともに比較し、それぞれの強みと適用場面を明らかにします。
まず、各ライブラリが採用する状態管理の基本方針を見てみましょう。
redux、redux-thunk、redux-saga、zustand、effector は、状態を直接変更せず、新しい状態オブジェクトを生成するイミュータブルなアプローチを採用しています。
// redux (with Redux Toolkit)
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1; } // Immerにより安全にミュート可能
}
});
// zustand
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }))
}));
// effector
import { createStore, createEvent } from 'effector';
const increment = createEvent();
const $counter = createStore(0).on(increment, (state) => state + 1);
mobx は、状態を直接変更(ミュート)しても、自動的に依存コンポーネントを再レンダリングします。
// mobx
import { makeAutoObservable } from 'mobx';
class Counter {
value = 0;
constructor() { makeAutoObservable(this); }
increment() { this.value += 1; }
}
const counter = new Counter();
recoil は、Atom(状態の最小単位)とSelector(派生状態)を組み合わせて、必要な部分だけを再計算・再レンダリングします。
// recoil
import { atom, selector, useRecoilState, useRecoilValue } from 'recoil';
const countAtom = atom({ key: 'count', default: 0 });
const doubledCount = selector({
key: 'doubledCount',
get: ({ get }) => get(countAtom) * 2
});
xstate は、状態遷移を明示的に定義し、不正な状態遷移を防ぎます。
// xstate
import { createMachine } from 'xstate';
const toggleMachine = createMachine({
id: 'toggle',
initial: 'inactive',
states: {
inactive: { on: { TOGGLE: 'active' } },
active: { on: { TOGGLE: 'inactive' } }
}
});
react-query は、UIローカル状態ではなく、サーバーから取得したデータのライフサイクルを管理します。
// react-query
import { useQuery } from '@tanstack/react-query';
function UserProfile({ userId }) {
const { data, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
});
// ...
}
次に、各ライブラリが非同期処理(例:API呼び出し)をどう扱うかを比較します。
redux-thunk は、アクションクリエーター内で非同期処理を直接記述できます。
// redux-thunk
const fetchUser = (userId) => async (dispatch) => {
dispatch({ type: 'FETCH_USER_START' });
try {
const user = await api.getUser(userId);
dispatch({ type: 'FETCH_USER_SUCCESS', payload: user });
} catch (error) {
dispatch({ type: 'FETCH_USER_FAILURE', error });
}
};
redux-saga は、ジェネレーター関数で非同期処理を宣言的に記述し、キャンセルや並列処理を容易にします。
// redux-saga
import { call, put, takeEvery, cancel } from 'redux-saga/effects';
function* fetchUserSaga(action) {
try {
const user = yield call(api.getUser, action.payload.userId);
yield put({ type: 'FETCH_USER_SUCCESS', user });
} catch (error) {
yield put({ type: 'FETCH_USER_FAILURE', error });
}
}
function* watchFetchUser() {
yield takeEvery('FETCH_USER_REQUEST', fetchUserSaga);
}
effector は、イベント、エフェクト、ストアを分離し、非同期処理を宣言的に接続します。
// effector
import { createEvent, createEffect, createStore } from 'effector';
const fetchUserFx = createEffect(async (userId) => {
return await api.getUser(userId);
});
const $user = createStore(null).on(fetchUserFx.doneData, (_, user) => user);
const fetchUser = createEvent();
fetchUserFx.watch((userId) => console.log('Fetching user:', userId));
mobx では、アクション内で非同期処理を直接記述できますが、flow ユーティリティを使うとジェネレーター風に書けます。
// mobx
import { flow } from 'mobx';
class UserStore {
user = null;
fetchUser = flow(function* (this: UserStore, userId) {
try {
this.user = yield api.getUser(userId);
} catch (error) {
console.error(error);
}
});
}
zustand は、ストアのセッター内で非同期処理を直接記述できます。
// zustand
const useStore = create((set) => ({
user: null,
fetchUser: async (userId) => {
try {
const user = await api.getUser(userId);
set({ user });
} catch (error) {
console.error(error);
}
}
}));
react-query は、queryFn に非同期処理を渡すだけで、自動的にローディング・エラー・再試行・キャッシュを処理します。
// react-query
const { data, error, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => api.getUser(userId),
retry: 3
});
UIでは、元の状態から計算された値(例:フィルタ済みリスト、合計金額)をよく使います。各ライブラリはこれをどう扱うでしょうか?
// redux
import { createSelector } from '@reduxjs/toolkit';
const selectItems = (state) => state.items;
const selectFilter = (state) => state.filter;
const selectFilteredItems = createSelector(
[selectItems, selectFilter],
(items, filter) => items.filter(item => item.category === filter)
);
// recoil
const filteredItems = selector({
key: 'filteredItems',
get: ({ get }) => {
const items = get(itemsAtom);
const filter = get(filterAtom);
return items.filter(item => item.category === filter);
}
});
// mobx
class Store {
items = [];
filter = '';
constructor() { makeAutoObservable(this); }
get filteredItems() {
return this.items.filter(item => item.category === this.filter);
}
}
zustand は、subscribe や useMemo で派生状態を実現しますが、公式推奨は zustand/middleware の combine や vanilla ストアの利用です。
// zustand
const useStore = create((set, get) => ({
items: [],
filter: '',
getFilteredItems: () => {
const { items, filter } = get();
return items.filter(item => item.category === filter);
}
}));
// effector
const $items = createStore([]);
const $filter = createStore('');
const $filteredItems = $items.combine($filter, (items, filter) =>
items.filter(item => item.category === filter)
);
react-query を使うべきです。キャッシュ、再取得、エラーハンドリングが自動で、他のローカル状態管理と併用可能です。zustand、redux、mobx など)を検討します。zustand か mobx。セットアップが簡単で、開発速度が速い。redux(Redux Toolkit)か effector。予測可能な状態管理と強力なデバッグ機能がチーム開発を助けます。xstate で状態遷移を明示的にモデリングすると、バグが減ります。recoil か mobx。不要な再レンダリングを避けられます。zustand で十分。Context APIの欠点を回避しつつ、オーバーヘッドが少ないです。recoil: Meta社が2023年に開発を一時停止しており、将来的なサポートに不透明性があります。新規プロジェクトでは慎重に検討すべきです。redux-thunk vs redux-saga: 単純な非同期処理なら redux-thunk で十分ですが、キャンセルや並列処理が必要なら redux-saga を選びましょう。mobx: ミュータブル操作は便利ですが、過度に使うと状態の追跡が難しくなるため、チーム内でのコーディング規約が重要です。| ライブラリ | 主な用途 | 学習コスト | 推奨シナリオ |
|---|---|---|---|
redux | 予測可能なグローバル状態管理 | 中 | 大規模アプリ、厳格なテスト要件 |
redux-thunk | Reduxの軽量非同期処理 | 低 | 単純なAPI呼び出し |
redux-saga | 複雑な非同期ワークフロー | 高 | キャンセル可能な処理、並列処理 |
zustand | 軽量グローバル状態管理 | 低 | 小〜中規模アプリ、素早い開発 |
mobx | ミュータブルなリアクティブ状態 | 中 | オブジェクト指向的な状態管理を好むチーム |
recoil | 細粒度な状態と派生状態 | 中 | 複雑な依存関係を持つUI(注意:開発停止中) |
effector | イベント駆動のビジネスロジック | 高 | 大規模アプリ、副作用の分離が必要な場合 |
xstate | 状態遷移の明示的モデリング | 中 | フォーム、モーダル、ゲームなど状態が明確なUI |
react-query | サーバー状態の管理 | 低 | あらゆるAPIデータの取得・キャッシュ(他のローカル状態管理と併用) |
react-query を導入する: サーバー状態の管理はほぼ必須です。zustand から始める: 小さく始め、必要に応じて redux や effector に移行できます。xstate を検討: 状態遷移図を描けるなら、コードでも表現すべきです。recoil は現状避ける: 開発停止中のため、新規プロジェクトではリスクが高いです。状態管理ライブラリは「銀の弾丸」ではありません。プロジェクトの要件、チームのスキル、将来の保守性を総合的に判断し、適切なツールを選びましょう。
reduxは、状態の変更を完全に予測可能にし、時間旅行デバッグや厳格なテストを必要とする大規模アプリケーションに適しています。ただし、ボイラープレートが多く、小規模プロジェクトでは開発速度を阻害する可能性があります。現在はRedux Toolkitにより定型コードは大幅に削減されていますが、それでも他の軽量ライブラリと比べると重めです。
react-queryは、サーバーから取得したデータのキャッシング、再取得、エラーハンドリングなど、サーバー状態の管理に特化しています。UIローカル状態ではなく、API経由で取得するデータを扱うプロジェクトでは、ほぼ必須の選択肢です。ローカル状態管理とは併用が前提であり、単体でアプリ全体の状態を管理する用途には向きません。
effectorは、イベント駆動で副作用を分離できる強力なリアクティブシステムを必要とするプロジェクトに適しています。Reduxのような予測可能な状態管理を維持しつつ、非同期処理やビジネスロジックを宣言的に記述したい場合に有効です。ただし、学習曲線がやや急で、小規模アプリにはオーバースペックになる可能性があります。TypeScriptとの親和性も高く、大規模チームでの保守性を重視する場合に選ぶと良いでしょう。
mobxは、オブジェクト指向的に状態を直接変更しながらも、自動的に再レンダリングをトリガーしたい場合に最適です。リアクティブなプログラミングモデルを好む開発者や、状態の更新ロジックをシンプルに保ちたいチームに向いています。ただし、過度なミュータブル操作はデバッグを難しくする可能性があるため、コードレビューと規約によるガードが必要です。
recoilは、AtomとSelectorという単位で細粒度な状態管理を実現し、派生状態の計算を効率的に行いたい場合に有効です。特に、複雑な依存関係を持つUI(例:フィルタ+ソート+検索)でパフォーマンスを最適化したい場合に適しています。ただし、Metaによって開発が一時停止されており、将来的なサポートに不安がある点は考慮すべきです。
redux-sagaは、複雑な非同期フロー(例:キャンセル可能なAPI呼び出し、並列処理、ウォッチ処理)を宣言的に記述したい場合に選ぶべきです。ジェネレーター関数を活用して副作用を分離できるため、テストしやすく、ビジネスロジックの可読性を高められます。ただし、ジェネレーターの理解が必要で、簡単な非同期処理にはオーバーエンジニアリングになりがちです。
redux-thunkは、Reduxで非同期アクションを簡単に導入したい場合に最適な軽量ミドルウェアです。API呼び出しなどの基本的な非同期処理を、既存のReduxフローに最小限の変更で組み込めます。ただし、複雑なワークフロー(例:キャンセル、並列処理)には向かず、そのような要件がある場合はredux-sagaやredux-observableを検討すべきです。
xstateは、UIの状態遷移が明確に定義できる(例:フォームのステップ、モーダルの表示状態、ゲームのシーン)場合に非常に強力です。有限状態機械(FSM)や状態チャートを用いて、バグの原因になりやすい曖昧な状態を排除できます。ただし、すべての状態を事前にモデリングする必要があり、柔軟性を求めるケースでは制約が強すぎるかもしれません。
zustandは、Reduxのような集中管理の利点を残しつつ、ボイラープレートをほぼゼロに抑えたい場合に最適です。シンプルなAPIでグローバル状態を管理でき、Context APIの欠点(再レンダリングの無駄)を回避します。小〜中規模アプリや、素早くプロトタイプを作りたいチームに強くおすすめできます。
Redux is a predictable state container for JavaScript apps.
It helps you write applications that behave consistently, run in different environments (client, server, and native), and are easy to test. On top of that, it provides a great developer experience, such as live code editing combined with a time traveling debugger.
You can use Redux together with React, or with any other view library. The Redux core is tiny (2kB, including dependencies), and has a rich ecosystem of addons.
Redux Toolkit is our official recommended approach for writing Redux logic. It wraps around the Redux core, and contains packages and functions that we think are essential for building a Redux app. Redux Toolkit builds in our suggested best practices, simplifies most Redux tasks, prevents common mistakes, and makes it easier to write Redux applications.
The recommended way to start new apps with React and Redux Toolkit is by using our official Redux Toolkit + TS template for Vite, or by creating a new Next.js project using Next's with-redux template.
Both of these already have Redux Toolkit and React-Redux configured appropriately for that build tool, and come with a small example app that demonstrates how to use several of Redux Toolkit's features.
# Vite with our Redux+TS template
# (using the `degit` tool to clone and extract the template)
npx degit reduxjs/redux-templates/packages/vite-template-redux my-app
# Next.js using the `with-redux` template
npx create-next-app --example with-redux my-app
We do not currently have official React Native templates, but recommend these templates for standard React Native and for Expo:
npm install @reduxjs/toolkit react-redux
For the Redux core library by itself:
npm install redux
For more details, see the Installation docs page.
The Redux core docs are located at https://redux.js.org, and include the full Redux tutorials, as well usage guides on general Redux patterns:
The Redux Toolkit docs are available at https://redux-toolkit.js.org, including API references and usage guides for all of the APIs included in Redux Toolkit.
The Redux Essentials tutorial is a "top-down" tutorial that teaches "how to use Redux the right way", using our latest recommended APIs and best practices. We recommend starting there.
The Redux Fundamentals tutorial is a "bottom-up" tutorial that teaches "how Redux works" from first principles and without any abstractions, and why standard Redux usage patterns exist.
The #redux channel of the Reactiflux Discord community is our official resource for all questions related to learning and using Redux. Reactiflux is a great place to hang out, ask questions, and learn - please come and join us there!
Redux is a valuable tool for organizing your state, but you should also consider whether it's appropriate for your situation. Please don't use Redux just because someone said you should - instead, please take some time to understand the potential benefits and tradeoffs of using it.
Here are some suggestions on when it makes sense to use Redux:
Yes, these guidelines are subjective and vague, but this is for a good reason. The point at which you should integrate Redux into your application is different for every user and different for every application.
For more thoughts on how Redux is meant to be used, please see:
The whole global state of your app is stored in an object tree inside a single store. The only way to change the state tree is to create an action, an object describing what happened, and dispatch it to the store. To specify how state gets updated in response to an action, you write pure reducer functions that calculate a new state based on the old state and the action.
Redux Toolkit simplifies the process of writing Redux logic and setting up the store. With Redux Toolkit, the basic app logic looks like:
import { createSlice, configureStore } from '@reduxjs/toolkit'
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
incremented: state => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1
},
decremented: state => {
state.value -= 1
}
}
})
export const { incremented, decremented } = counterSlice.actions
const store = configureStore({
reducer: counterSlice.reducer
})
// Can still subscribe to the store
store.subscribe(() => console.log(store.getState()))
// Still pass action objects to `dispatch`, but they're created for us
store.dispatch(incremented())
// {value: 1}
store.dispatch(incremented())
// {value: 2}
store.dispatch(decremented())
// {value: 1}
Redux Toolkit allows us to write shorter logic that's easier to read, while still following the original core Redux behavior and data flow.
You can find the official logo on GitHub.
This project adheres to Semantic Versioning. Every release, along with the migration instructions, is documented on the GitHub Releases page.