Redux Promise Middlewareの完全ガイド:非同期状態管理の入門から応用まで

Redux Promise Middlewareの完全ガイド:非同期状態管理の入門から応用まで

【無料ダウンロードリンク】redux-promise-middlewareは、Reduxにおける非同期アクションクリエーターのシンプルかつ堅牢な処理を可能にします。プロジェクトページ: https://gitcode.com/gh_mirrors/re/redux-promise-middleware

Reduxにおける非同期処理に特化したミドルウェアが必要な理由

Reduxで非同期データフローを扱う際に悩まされたことはありませんか?複数のAPIリクエストが相互に関連する場合、コールバック地獄に陥る可能性があります。Redux Promise Middleware(以下RPM)は、こうした課題に対する洗練された解決策を提供し、非同期状態管理を単純かつ予測可能にします。

この記事を読んだ後、以下のスキルを習得できます:

  • RPMの動作原理と他の非同期ミドルウェアとの違いを理解する
  • RPMを含むRedux開発環境をすばやく構築する
  • 基本的な非同期アクションの作成と状態処理を習得する
  • パフォーマンス向上とエラーハンドリングの高度な機能(オプティミスティック更新、エラーハンドリング、TypeScript統合など)を実装する
  • 複雑なシナリオにおける非同期データフロー(並列リクエスト、連鎖呼び出し)を最適化する
  • RPMの一般的なエラーとパフォーマンスの落とし穴を回避する

Redux非同期ミドルウェアの比較分析

特徴 Redux Promise Middleware redux-promise redux-thunk redux-saga
基本アプローチ Promiseベース Promiseベース 関数クロージャー ジェネレータ関数
ミドルウェアサイズ 約2KB (gzip) 約1KB (gzip) 約2KB (gzip) 約15KB (gzip)
非同期状態の追跡 PENDING/FULFILLED/REJECTEDを自動生成 FULFILLED/REJECTEDのみ 手動実装 完全な制御
エラーハンドリング 組み込みエラー処理 手動での処理必要 try/catch必要 強力なエラーキャッチ機能
学習難易度 ★☆☆☆ ★☆☆☆ ★★☆☆ ★★★★
適用範囲 中規模非同期処理 シンプルな非同期要件 複雑なロジック制御 大規模アプリケーションの複雑なフロー

RPMの特徴は、各非同期アクションに対して自動的に以下の3つの状態アクションを生成することです:

  • PENDING:非同期操作開始時に発火
  • FULFILLED:非同期操作完了時に発火
  • REJECTED:非同期操作失敗時に発火

この仕組みにより、コンポーネントは非同期操作の各段階を正確に追跡でき、ローディング表示やオプティミスティック更新といった高度なUIインタラクションを実現できます。

スタートガイド:5分でRPM開発環境を構築

コア依存ライブラリのインストール

# npmを使用
npm install redux-promise-middleware --save

# yarnを使用
yarn add redux-promise-middleware

基本設定例

// store.js
import { createStore, applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise-middleware';
import rootReducer from './reducers';

// RPMを含むミドルウェア配列を作成
const middleware = [
  promiseMiddleware // デフォルト設定
];

// ミドルウェアを適用
const store = createStore(
  rootReducer,
  applyMiddleware(...middleware)
);

export default store;

カスタム設定例(上級者向け)

// store.js
import { createStore, applyMiddleware } from 'redux';
import { createPromise } from 'redux-promise-middleware';

// カスタム設定でRPMを作成
const customPromiseMiddleware = createPromise({
  promiseTypeSuffixes: ['LOADING', 'SUCCESS', 'ERROR'], // カスタム状態接尾辞
  promiseTypeDelimiter: '/' // スラッシュを区切り文字として使用(デフォルトはアンダースコア)
});

const store = createStore(
  rootReducer,
  applyMiddleware(customPromiseMiddleware)
);

export default store;

基本概念:RPMの動作メカニズム

非同期アクションライフサイクル

標準的なアクション構造

RPMはFlux標準Action規格に従い、有効な非同期アクションには以下の要素が必要です:

  • type: アクションタイプ文字列(必須)
  • payload: PromiseオブジェクトまたはPromiseを含むオブジェクト(必須)
  • meta: 追加のメタデータ(任意)

実践ガイド:基本から高度な機能まで

1. 基本的な非同期アクションの作成

// actions/dogActions.js
export const fetchRandomDog = () => ({
  type: 'FETCH_RANDOM_DOG',
  payload: fetch('https://dog.ceo/api/breeds/image/random')
    .then(response => response.json())
});

2. Async/Await構文の使用

// actions/userActions.js
export const fetchUserProfile = (userId) => ({
  type: 'FETCH_USER_PROFILE',
  async payload() {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) throw new Error('ユーザー取得に失敗しました');
    return response.json();
  }
});

3. 対応するReducerの構築

// reducers/dogReducer.js
const initialState = {
  imageUrl: null,
  isLoading: false,
  error: null
};

export default function dogReducer(state = initialState, action) {
  switch (action.type) {
    case 'FETCH_RANDOM_DOG_PENDING':
      return {
        ...state,
        isLoading: true,
        error: null
      };
      
    case 'FETCH_RANDOM_DOG_FULFILLED':
      return {
        ...state,
        isLoading: false,
        imageUrl: action.payload.message
      };
      
    case 'FETCH_RANDOM_DOG_REJECTED':
      return {
        ...state,
        isLoading: false,
        error: action.payload.message
      };
      
    default:
      return state;
  }
}

4. コンポーネントでの非同期状態利用

// components/DogViewer.jsx
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchRandomDog } from '../actions/dogActions';

export default function DogViewer() {
  const dispatch = useDispatch();
  const { imageUrl, isLoading, error } = useSelector(state => state.dog);
  
  const handleFetchDog = () => {
    dispatch(fetchRandomDog());
  };
  
  if (isLoading) return <div>読み込み中...</div>;
  if (error) return <div>エラー: {error}</div>;
  
  return (
    <div>
      <button onClick={handleFetchDog} disabled={isLoading}>
        ランダム犬画像を取得
      </button>
      {imageUrl && ![Random dog]({imageUrl})}
    </div>
  );
}

5. エラーハンドリングのベストプラクティス

// actions/dataActions.js
export const fetchImportantData = () => ({
  type: 'FETCH_IMPORTANT_DATA',
  payload: new Promise((resolve, reject) => {
    // APIリクエストの模擬
    setTimeout(() => {
      const success = Math.random() > 0.5;
      if (success) {
        resolve({ data: '重要なデータ' });
      } else {
        // スタックトレースのためにErrorオブジェクトを使用
        reject(new Error('データ取得に失敗しました。再試行してください'));
      }
    }, 1000);
  })
});

// コンポーネントでの処理
const dispatch = useDispatch();
const handleFetch = () => {
  dispatch(fetchImportantData())
    .catch(error => {
      // グローバルエラーハンドリングをここに追加可能(例:エラーログ送信)
      logErrorToService(error);
      // ユーザーフレンドリーなエラーメッセージを表示
      setErrorMessage(error.message);
    });
};

6. 高度な機能:オプティミスティック更新

オプティミスティック更新は、サーバーが操作を確認する前にUIを更新する技術で、ユーザー体験を向上させます。

// actions/todoActions.js
export const addTodoOptimistic = (text) => ({
  type: 'ADD_TODO_OPTIMISTIC',
  payload: {
    // 非同期操作
    promise: api.addTodo(text),
    // オプティミスティック更新データ
    data: {
      id: Date.now(), // 一時ID、サーバーでは別のIDが返される可能性あり
      text,
      completed: false
    }
  }
});

// reducers/todoReducer.js
case 'ADD_TODO_OPTIMISTIC_PENDING':
  // payload.dataを使ってオプティミスティック更新
  return {
    ...state,
    todos: [...state.todos, action.payload],
    isLoading: true
  };
  
case 'ADD_TODO_OPTIMISTIC_FULFILLED':
  // サーバーから返された実際のデータに置き換え
  return {
    ...state,
    todos: state.todos.map(todo => 
      todo.id === action.payload.tempId ? action.payload : todo
    ),
    isLoading: false
  };
  
case 'ADD_TODO_OPTIMISTIC_REJECTED':
  // オプティミスティック更新失敗、状態をロールバック
  return {
    ...state,
    todos: state.todos.filter(todo => todo.id !== action.meta.tempId),
    isLoading: false,
    error: action.payload.message
  };

7. 連鎖アクション呼び出し

// actions/chainActions.js
import { fetchUser } from './userActions';
import { fetchUserPosts } from './postActions';

// Redux Thunkでアクションの連鎖呼び出しを実装
export const fetchUserAndPosts = (userId) => {
  return async (dispatch) => {
    try {
      // 最初にユーザー情報を取得
      const { value: user } = await dispatch(fetchUser(userId));
      
      // ユーザーIDを使って投稿を取得
      const { value: posts } = await dispatch(fetchUserPosts(user.id));
      
      return { user, posts };
    } catch (error) {
      // エラー集中処理
      console.error('ユーザー情報取得に失敗:', error);
      throw error;
    }
  };
};

// コンポーネントでの使用
const loadUser = async (userId) => {
  setLoading(true);
  try {
    const { user, posts } = await dispatch(fetchUserAndPosts(userId));
    setUser(user);
    setPosts(posts);
  } catch (error) {
    setError(error.message);
  } finally {
    setLoading(false);
  }
};

8. TypeScriptとの完全互換性

// store.ts
import { createStore, applyMiddleware, Action } from 'redux';
import promiseMiddleware, { FluxStandardAction } from 'redux-promise-middleware';

// ステートインターフェース定義
interface AppState {
  user: {
    data: User | null;
    isLoading: boolean;
    error: Error | null;
  };
}

// アクション型定義
type UserActions = 
  | FluxStandardAction<'FETCH_USER_PENDING', void>
  | FluxStandardAction<'FETCH_USER_FULFILLED', User>
  | FluxStandardAction<'FETCH_USER_REJECTED', Error>;

// 非同期アクションの作成
export const fetchUser = (userId: number): UserActions => ({
  type: 'FETCH_USER',
  payload: api.fetchUser(userId)
});

// Reducer
const userReducer = (
  state: AppState['user'] = { data: null, isLoading: false, error: null },
  action: UserActions
): AppState['user'] => {
  switch (action.type) {
    case 'FETCH_USER_PENDING':
      return { ...state, isLoading: true, error: null };
    case 'FETCH_USER_FULFILLED':
      return { data: action.payload, isLoading: false, error: null };
    case 'FETCH_USER_REJECTED':
      return { ...state, isLoading: false, error: action.payload };
    default:
      return state;
  }
};

パフォーマンス最適化とベストプラクティス

1. 重複リクエストの回避

// リクエストディバウンス付きアクションクリエーター
export const fetchDataWithDebounce = (params) => {
  return (dispatch, getState) => {
    const state = getState();
    // 同じリクエストが進行中かどうかを確認
    if (state.data.isLoading && isEqual(state.data.lastParams, params)) {
      // 進行中の場合は既存のPromiseを返して重複を避ける
      return state.data.currentRequest;
    }
    
    // 新しいリクエストを作成
    const requestPromise = api.fetchData(params);
    
    // 後続の確認のためにリクエスト情報を保存
    dispatch({
      type: 'SET_CURRENT_REQUEST',
      payload: { params, request: requestPromise }
    });
    
    // 実際の非同期アクションを発行
    return dispatch({
      type: 'FETCH_DATA',
      payload: requestPromise
    });
  };
};

2. 古いリクエストのキャンセル

ユーザーが高速でタブを切り替える場合、以前のリクエストは不要になり、結果を処理すると状態が不整合になることがあります。

// AbortControllerを使用してFetchリクエストをキャンセル
export const fetchSearchResults = (query) => {
  return (dispatch) => {
    // AbortControllerを作成
    const abortController = new AbortController();
    
    // キャンセル用にコントローラーを保存
    dispatch({
      type: 'STORE_ABORT_CONTROLLER',
      payload: { query, controller: abortController }
    });
    
    return dispatch({
      type: 'FETCH_SEARCH_RESULTS',
      payload: fetch(`/api/search?q=${query}`, {
        signal: abortController.signal
      }).then(response => response.json())
    });
  };
};

// コンポーネントがアンマウントまたはクエリ変更時にリクエストをキャンセル
useEffect(() => {
  return () => {
    // 以前のリクエストをキャンセル
    dispatch({ type: 'CANCEL_PENDING_REQUESTS' });
  };
}, [dispatch]);

3. ミドルウェアの順序と組み合わせ

Reduxミドルウェアの順序は重要です。正しい順序は:ログミドルウェア → RPM → Thunkです。

// 正しいミドルウェアの順序
const middleware = [
  logger,          // ログミドルウェアは最初
  promiseMiddleware, // RPMミドルウェア
  thunk            // Thunkミドルウェアは最後
];

const store = createStore(
  rootReducer,
  applyMiddleware(...middleware)
);

4. セレクターとの組み合わせ

// selectors/dataSelectors.js
import { createSelector } from 'reselect';

// 基本セレクター
const selectDataState = state => state.data;

// メモ化セレクターを作成
export const selectFilteredData = createSelector(
  [selectDataState, (state, filter) => filter],
  (dataState, filter) => {
    // dataStateまたはfilterが変更されたときのみ再計算
    if (dataState.isLoading || dataState.error) return [];
    
    return dataState.items.filter(item => 
      item.category === filter.category && 
      item.value > filter.minValue
    );
  }
);

// コンポーネントで使用
const filteredItems = useSelector(state => 
  selectFilteredData(state, currentFilter)
);

一般的な問題と解決方法

Q1: アクションタイプの競合はどう対処しますか?

A: 名前空間やモジュールプレフィックスを使用します:

// 異なるモジュールで異なるプレフィックスを使用
// userActions.js
export const fetchUser = () => ({
  type: 'user/FETCH_USER', // /userプレフィックスを使用
  payload: api.fetchUser()
});

// postActions.js
export const fetchPost = () => ({
  type: 'post/FETCH_POST', // /postプレフィックスを使用
  payload: api.fetchPost()
});

Q2: 複雑な非同期フローをどう扱いますか?

A: 非常に複雑なフローの場合はRedux Sagaと組み合わせることも可能ですが、多くの場合、RPM + Thunkで十分です:

// Thunkで複雑な条件ロジックを処理
export const complexDataFlow = (params) => {
  return async (dispatch, getState) => {
    const { auth, data } = getState();
    
    // ユーザー認証を確認
    if (!auth.isAuthenticated) {
      // まず認証
      await dispatch(authenticate());
    }
    
    // キャッシュを確認
    if (data.cache[params.id] && Date.now() - data.cache[params.id].timestamp < 300000) {
      // キャッシュデータを返す
      return dispatch({
        type: 'COMPLEX_FLOW_CACHE_HIT',
        payload: data.cache[params.id].data
      });
    }
    
    // 主要な非同期操作を実行
    const result = await dispatch({
      type: 'COMPLEX_FLOW_MAIN_OPERATION',
      payload: api.complexOperation(params)
    });
    
    // 後続操作を実行
    await dispatch(processResult(result.value));
    
    return result;
  };
};

Q3: RPMを使用したReduxコードのテスト方法は?

A: JestとRedux Mock Storeを使用します:

// actions/__tests__/dataActions.test.js
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import promiseMiddleware from 'redux-promise-middleware';
import { fetchData } from '../dataActions';
import api from '../../api';

// ミドルウェア付きのmock storeを作成
const mockStore = configureMockStore([thunk, promiseMiddleware]);

// APIをモック
jest.mock('../../api');

describe('fetchData action', () => {
  it('成功時にFULFILLEDアクションをディスパッチする', async () => {
    // APIの成功レスポンスをモック
    api.fetchData.mockResolvedValue({ data: 'テスト' });
    
    const expectedActions = [
      { type: 'FETCH_DATA_PENDING' },
      { type: 'FETCH_DATA_FULFILLED', payload: { data: 'テスト' } }
    ];
    
    const store = mockStore({});
    
    await store.dispatch(fetchData());
    
    // 発行されたアクションを検証
    expect(store.getActions()).toEqual(expectedActions);
  });
});

バージョン移行ガイド

v5からv6への主要な変更点

  1. デフォルトエクスポートの変更:v6ではデフォルトエクスポートは事前設定されたミドルウェアであり、カスタム設定にはcreatePromiseを使用する必要があります
// v5でのカスタム設定
import promiseMiddleware from 'redux-promise-middleware';
const middleware = promiseMiddleware({
  promiseTypeSuffixes: ['LOADING', 'SUCCESS', 'ERROR']
});

// v6でのカスタム設定
import { createPromise } from 'redux-promise-middleware';
const middleware = createPromise({
  promiseTypeSuffixes: ['LOADING', 'SUCCESS', 'ERROR']
});

  1. アクション構造の変更:v6はFSA規格を厳密に遵守し、エラーActionにはerror: true属性が含まれます
// v6でのエラーAction構造
{
  type: 'ACTION_REJECTED',
  payload: new Error('エラーメッセージ'),
  error: true // 新たに追加されたエラー識別子
}

  1. TypeScriptサポート:v6は完全なTypeScript型定義を提供し、別途@types/redux-promise-middlewareのインストールは不要です

まとめと将来展望

Redux Promise Middlewareは、非同期状態Actionを自動生成することで、Reduxアプリケーションにおける非同期処理の管理を大幅に簡略化します。使いやすさと機能性のバランスを取っており、redux-thunkのように多くのテンプレートコードを書く必要もなく、redux-sagaのように学習曲線が急なわけでもありません。

Reactエコシステムの進化とともに、React QueryやSWRなどのデータ取得ライブラリ、Redux ToolkitのcreateAsyncThunkなど新しい非同期データフローの解決策が登場しています。これらはそれぞれ利点がありますが、RPMは軽量で柔軟な選択肢であり、既存のReduxプロジェクトに最適です。

今後の学習提案

  • Redux ToolkitがRedux開発をどのように簡素化するかを調べる
  • Immerなどの不変データ処理技術を学ぶ
  • サーバーサイドレンダリングにおける非同期データプリロード戦略を研究する
  • ZustandやJotaiなどの原子レベル状態管理ライブラリの新しい傾向を理解する

Redux Promise Middlewareを習得することで、現在のプロジェクトの非同期管理問題を解決し、Reduxミドルウェアの仕組みを深く理解し、より複雑なReduxエコシステムツールの基礎を築くことができます。

【無料ダウンロードリンク】redux-promise-middleware Enables simple, yet robust handling of async action creators in Redux 项目地址: https://gitcode.com/gh_mirrors/re/redux-promise-middleware

タグ: Redux Promise Middleware 非同期処理 React

7月9日 21:57 投稿