redux-observable vs redux-saga vs redux-thunk
Redux 非同期処理ミドルウェアの比較と選定ガイド
redux-observableredux-sagaredux-thunk類似パッケージ:

Redux 非同期処理ミドルウェアの比較と選定ガイド

redux-thunk、redux-saga、redux-observable は、Redux アプリケーションで非同期処理やサイドエフェクトを管理するためのミドルウェアです。redux-thunk は関数を返す関数を使う最もシンプルで標準的なアプローチです。redux-saga はジェネレーター関数を使い、複雑なフローやキャンセル処理を宣言的に記述できます。redux-observable は RxJS をベースにし、ストリームベースのイベント処理や高度な合成を可能にします。これらはそれぞれ学習コストと表現力が異なり、プロジェクトの規模や要件に合わせて選択する必要があります。

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

3 年

GitHub Starsランキング

統計詳細

パッケージ
ダウンロード数
Stars
サイズ
Issues
公開日時
ライセンス
redux-observable07,80368.4 kB5410ヶ月前MIT
redux-saga0-36.8 kB-3ヶ月前MIT
redux-thunk017,65626.8 kB23年前MIT

Redux 非同期ミドルウェア徹底比較:Thunk vs Saga vs Observable

Redux で非同期処理やサイドエフェクトを扱う際、redux-thunk、redux-saga、redux-observable の 3 つが主要な選択肢です。これらはすべて Redux のアクションフローに介入し、外部 API 呼び出しや複雑なロジックを処理しますが、そのアプローチと適性は大きく異なります。現場での実装経験に基づき、技術的な違いと選定基準を解説します。

🚀 基本的な非同期処理の実装

最も基本的な「API からデータを取得してストアに保存する」処理を、各ミドルウェアでどのように記述するか比較します。

redux-thunk は、アクションクリエイターが関数を返すようにします。この関数は dispatch と getState を引数に取れます。

// redux-thunk
export const fetchUser = (userId) => {
  return async (dispatch, getState) => {
    dispatch({ type: 'FETCH_USER_REQUEST' });
    try {
      const response = await api.getUser(userId);
      dispatch({ type: 'FETCH_USER_SUCCESS', payload: response });
    } catch (error) {
      dispatch({ type: 'FETCH_USER_FAILURE', error });
    }
  };
};

redux-saga は、ジェネレーター関数(function*)を使います。takeLatest などのヘルパーを使うと、並列処理の制御が容易です。

// redux-saga
import { call, put, takeLatest } from 'redux-saga/effects';

function* fetchUserWorker(action) {
  try {
    const user = yield call(api.getUser, action.payload);
    yield put({ type: 'FETCH_USER_SUCCESS', payload: user });
  } catch (error) {
    yield put({ type: 'FETCH_USER_FAILURE', error });
  }
}

export function* watchFetchUser() {
  yield takeLatest('FETCH_USER_REQUEST', fetchUserWorker);
}

redux-observable は、RxJS の Observable を使います。アクションのストリームを監視し、新しいストリームを返すエピックを定義します。

// redux-observable
import { ofType } from 'redux-observable';
import { mergeMap, map, catchError } from 'rxjs/operators';
import { of } from 'rxjs';

const fetchUserEpic = (action$, state$) =>
  action$.pipe(
    ofType('FETCH_USER_REQUEST'),
    mergeMap(action =>
      api.getUser(action.payload).pipe(
        map(user => ({ type: 'FETCH_USER_SUCCESS', payload: user })),
        catchError(error => of({ type: 'FETCH_USER_FAILURE', error }))
      )
    )
  );

🛑 キャンセルと競合制御

ユーザーが素早く入力する検索ボックスなどで、古いリクエストをキャンセルしたり、最新のレスポンスのみを処理したりするケースは頻出します。

redux-thunk では、キャンセル処理を自前で実装する必要があります。AbortController を使うなどが一般的ですが、ミドルウェアレベルの支援はありません。

// redux-thunk
export const searchItems = (query) => {
  return async (dispatch, getState) => {
    const controller = new AbortController();
    // キャンセルトークンの管理を自前で行う必要がある
    dispatch({ type: 'SEARCH_REQUEST', meta: { controller } });
    try {
      const result = await api.search(query, { signal: controller.signal });
      dispatch({ type: 'SEARCH_SUCCESS', payload: result });
    } catch (error) {
      if (error.name !== 'AbortError') {
        dispatch({ type: 'SEARCH_FAILURE', error });
      }
    }
  };
};

redux-saga は、キャンセル処理が組み込まれています。takeLatest を使えば、新しいアクションが発生した時点で進行中のタスクを自動的にキャンセルします。

// redux-saga
import { takeLatest } from 'redux-saga/effects';

// 最新の search リクエストのみを処理し、以前のリクエストは自動キャンセル
export function* watchSearch() {
  yield takeLatest('SEARCH_REQUEST', searchWorker);
}

redux-observable は、RxJS の演算子を使って柔軟に制御できます。switchMap を使うと、新しいソース値が来た時点で前の内部 Observable をキャンセルします。

// redux-observable
import { switchMap } from 'rxjs/operators';

const searchEpic = (action$) =>
  action$.pipe(
    ofType('SEARCH_REQUEST'),
    // 新しい検索リクエストが来たら、前のリクエストをキャンセル
    switchMap(action =>
      api.search(action.payload).pipe(
        map(result => ({ type: 'SEARCH_SUCCESS', payload: result }))
      )
    )
  );

🧪 テストの書きやすさ

ミドルウェアのロジックをユニットテストする方法も選定に影響します。

redux-thunk は通常の関数なので、モックした dispatch と getState を渡してテストします。非同期は async/await で待ちます。

// redux-thunk test
it('dispatches success on fetch', async () => {
  const dispatch = jest.fn();
  const getState = () => ({});
  await fetchUser(1)(dispatch, getState);
  expect(dispatch).toHaveBeenCalledWith({ type: 'FETCH_USER_SUCCESS', ... });
});

redux-saga はジェネレーターを 1 ステップずつ進めてテストします。yield された効果を断言します。

// redux-saga test
it('calls api and puts success', () => {
  const iterator = fetchUserWorker({ payload: 1 });
  expect(iterator.next().value).toEqual(call(api.getUser, 1));
  expect(iterator.next({ name: 'John' }).value).toEqual(
    put({ type: 'FETCH_USER_SUCCESS', payload: { name: 'John' } })
  );
});

redux-observable は、テスト用の TestScheduler や、アクションのストリームを流し込んで出力を監視する手法を取ります。

// redux-observable test
it('maps request to success', () => {
  const actions = of({ type: 'FETCH_USER_REQUEST', payload: 1 });
  const epic = fetchUserEpic(actions);
  epic.subscribe(result => {
    expect(result.type).toBe('FETCH_USER_SUCCESS');
  });
});

📦 依存関係と学習コスト

各ライブラリが求めるスキルセットとプロジェクトへの影響です。

  • redux-thunk: 追加の依存関係はほぼありません(Redux Toolkit に含まれています)。JavaScript の非同期関数の知識だけで書けるため、学習コストは最低です。
  • redux-saga: 独自のジェネレーターパターンを学ぶ必要があります。call、put、take などのエフェクトを理解する必要があり、初期の学習コストは中程度です。
  • redux-observable: RxJS の知識が必須です。Observable、Operator、Subject などの概念を理解していないと、コードを読むことも書くことも困難です。学習コストは最も高いです。

📊 比較サマリー

特徴redux-thunkredux-sagaredux-observable
基本構文関数(async/await)ジェネレーター関数RxJS Observable
学習コスト🟢 低い🟡 中程度🔴 高い
キャンセル処理自前実装が必要🟢 組み込み(takeLatest 等)🟢 演算子(switchMap 等)
テスト通常の非同期テストジェネレーター進行テストストリームテスト
依存関係ほぼなしredux-sagarxjs
適した規模小〜中規模中〜大規模大規模・複雑なイベント処理

💡 最終的な推奨事項

プロジェクトの要件とチームのスキルセットに合わせて以下のように選定してください。

redux-thunk は、Redux Toolkit を使っている場合のデファクトスタンダードです。特別な理由がない限り、まずはこれを選ぶべきです。大多数のアプリケーションの非同期要件はこれで十分に満たせます。

redux-saga は、複雑なビジネスロジックや、通信のキャンセル、並列処理の制御など、Thunk では管理が難しくなるケースで力を発揮します。大規模なエンタープライズアプリでよく採用されます。

redux-observable は、すでに RxJS を使っているプロジェクトや、WebSocket やユーザー入力ストリームなど、イベントの流れを細かく制御する必要がある場合に最適です。ただし、チーム全員が RxJS を理解できることが前提となります。

結論として、シンプルさと保守性を求めるなら redux-thunk、複雑なフロー制御が必要なら redux-saga、ストリーム処理が中心なら redux-observable を選定するのが賢明です。

選び方: redux-observable vs redux-saga vs redux-thunk

  • redux-observable:

    RxJS に習熟しており、イベントストリームの合成やdebounce などの操作を頻繁に行う場合に選択します。リアルタイム性が求められる機能や、複数のアクションストリームを組み合わせる処理に適しています。

  • redux-saga:

    複雑な非同期フローや、処理のキャンセル、競合制御が必要な場合に選択します。ジェネレーター関数に慣れているチームや、テスト容易性を重視する大規模アプリケーションに向いています。

  • redux-thunk:

    シンプルさ最優先の場合に選択します。Redux Toolkit の標準であり、学習コストが最も低いです。単純な API リクエストや、複雑なストリーム処理が必要ないプロジェクトに適しています。

redux-observable のREADME

Discord build status npm version npm downloads

RxJS-based middleware for Redux. Compose and cancel async actions to create side effects and more.

https://redux-observable.js.org