Redux Sagaは、Reduxのミドルウェアライブラリであり、アプリケーション内の副作用(side effect)をシンプルかつ簡単に管理できます。ES6のジェネレーター機能(function*
)を最大限に活用することで、非同期コードを同期コードのように記述できます。
Redux SagaはJavaScriptの世界だけでなく、デザインパターンとしても認識されています。Sagaパターンは、多くの副作用や潜在的なリスクを伴う長期トランザクション(処理)を管理する方法です。各トランザクションが成功した場合、問題が発生した際にトランザクションを初期状態に戻すためのカウンターアクションが必要です。
Sagaパターンの概念図
副作用とは、データの取得のためにAPIを呼び出したり、データをlocalStorageに保存したり、ブラウザからCookieを読み取ったりするなど、外部と対話する必要があるプログラミング関数内のタスクです。これらのタスクは通常、非同期であり、アプリケーションの状態を変更する可能性があります。ReduxのReducerは、同期(synchronous)および純粋(pure)である必要があります。つまり、入力に基づいてロジックを処理し、副作用を引き起こすことなく出力を返す必要があります。そのため、Redux SagaはReducerの外部で副作用を処理するために使用され、Reducerをシンプルでテストしやすく保ちます。
ジェネレーター関数は、JavaScriptの特殊な関数であり、実行を一時停止して値を複数回返すことができます。キーワードyield
は、実行を一時停止して値を返すために使用されます。一度実行して結果を返す通常の関数とは異なり、ジェネレーター関数は実行、結果を返して一時停止、そして停止した場所から実行を再開できます。
Redux Sagaは、ディスパッチされたアクションをリッスンすることによって動作します。アクションがディスパッチされると、Redux Sagaはそのアクションが関係するアクションかどうかを確認します。該当する場合、対応するジェネレーター関数を実行します。このジェネレーター関数は、エフェクトオブジェクトをyield
します。エフェクトオブジェクトは、非同期関数の呼び出しやストアへの新しいアクションのputなど、Reduxのミドルウェアに他の操作を実行するための指示を含むオブジェクトです。
では、なぜRedux Sagaを使用する必要があるのでしょうか?副作用を処理するためによく使用される別のミドルウェアであるRedux Thunkと比較して、Redux Sagaは多くの利点をもたらします。Redux Thunkは、複数の非同期タスクがネストされている場合、「コールバック地獄」を引き起こし、コードの可読性と保守性を低下させる可能性があります。Redux Sagaは、ジェネレーター関数とエフェクトオブジェクトを使用することで、コードを読みやすく、理解しやすく、テストしやすくします。Redux Sagaを使用すると、非同期フローを簡単にテストし、アクションを常に純粋に保つことができます。
APIリクエストの処理におけるRedux ThunkとRedux Sagaの比較例:
Redux Thunk:
import { API_BUTTON_CLICK, API_BUTTON_CLICK_SUCCESS, API_BUTTON_CLICK_ERROR } from './actions/consts';
import { getDataFromAPI } from './api';
const getDataStarted = () => ({ type: API_BUTTON_CLICK });
const getDataSuccess = data => ({ type: API_BUTTON_CLICK_SUCCESS, payload: data });
const getDataError = message => ({ type: API_BUTTON_CLICK_ERROR, payload: message });
const getDataFromAPI = () => {
return dispatch => {
dispatch(getDataStarted());
getDataFromAPI()
.then(data => {
dispatch(getUserSuccess(data));
})
.catch(err => {
dispatch(getDataError(err.message));
});
};
};
Redux Saga:
import { call, put, takeEvery } from 'redux-saga/effects';
import { API_BUTTON_CLICK, API_BUTTON_CLICK_SUCCESS, API_BUTTON_CLICK_ERROR } from './actions/consts';
import { getDataFromAPI } from './api';
export function* apiSideEffect(action) {
try {
const data = yield call(getDataFromAPI);
yield put({ type: API_BUTTON_CLICK_SUCCESS, payload: data });
} catch (e) {
yield put({ type: API_BUTTON_CLICK_ERROR, payload: e.message });
}
}
export function* apiSaga() {
yield takeEvery(API_BUTTON_CLICK, apiSideEffect);
}
Redux Sagaは、副作用処理ロジックをコンポーネントとReducerから分離し、コードの保守、拡張、およびテストを容易にします。Redux Sagaでtry/catch
を使用することで、Redux Thunkでpromiseチェーンを使用するよりもエラー処理が容易になります。