はじめに
新しい現場で状態管理にReduxを使っているのですが、ポートフォリオ作成以来触れていなかったので復習することにしました。
今回はredux-sagaのtakeEvery, takeLatestの挙動を確かめました。
redux-sagaとは
redux-sagaは、React/Reduxアプリケーションにおけるデータ通信などの非同期処理や、ブラウザキャッシュへのアクセス処理を簡単に実装するためのライブラリです。
同じReduxミドルウェアのredux-thunkでデータ通信処理を実装しようとすると、Action Creatorの中で別のAction Creatorを呼び出すなどのいわゆるコールバック地獄に陥る可能性があり、ピュアな(同じ引数を与えたときに同じ結果を返す)関数ではなくなってしまいます。
一方、redux-sagaではアプリケーションの中で副作用を個別に実行する独立したスレッド(Saga)をつくります。
そのため、Reduxとは独立して非同期処理を実行することになります。
redux-thunkのときのようにAction Creatorを汚さずに済み、テストも書きやすくなります。
実装
カウントボタンを用意し、Add 1ボタンを押したときの挙動を確かめました。

Viewコンポーネントの作成
結果値、Add 1ボタン、Minus 1ボタンが縦に並ぶような画面を作成します。
mapToPropsの実装がめんどくさいイメージがあったのですが、useSelectorやuseDispatchという便利なHooksがあることを知り、これらを使ってみました(とても簡単にかけた...)。
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import Card from '../card/card.component';
export const SagasExample = () => {
const value = useSelector((state) => state.app.value);
const dispatch = useDispatch();
return (
<Card>
{value}
<button onClick={() => dispatch({ type: 'INCREMENT' })}>Add 1</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>Minus 1</button>
</Card>
);
};
Middlewareの登録
まず、store.jsでredux-sagaからインポートしたcreateSagaMiddlewareでsagaMiddlewareを作成し、middlewareに登録します(ActionとStateのログを監視するためにredux-loggerも登録しています)。
sagaMiddleware.run(incrementSaga)で後に作成するincrementSagaを実行します。
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import logger from 'redux-logger';
import rootReducer from './root-reducer';
import { incrementSaga } from './app.saga';
const sagaMiddleware = createSagaMiddleware();
const middlewares = [logger, sagaMiddleware];
export const store = createStore(rootReducer, applyMiddleware(...middlewares));
sagaMiddleware.run(incrementSaga);
export default store;
また、rootReducerの中身とappReducerは以下のようになっています。
import { combineReducers } from 'redux';
import appReducer from './app.reducer';
const rootReducer = combineReducers({
app: appReducer
});
export default rootReducer;
const INITIAL_STATE = {
value: 0
};
const appReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case 'INCREMENT_FROM_SAGA':
return {
...state,
value: state.value + 1
};
case 'DECREMENT':
return {
...state,
value: state.value - 1
};
default:
return state;
}
};
export default appReducer;
TaskとTask Watcherの作成
Taskとして、3000 ms後にput({ type: 'INCREMENT_FROM_SAGA' })を実行(putはdispatchと同じ)するonIncrement()作成し、Task Watcherとして、incrementSaga()を作成します。
import { takeEvery, delay, put } from 'redux-saga/effects';
// Task
export function* onIncrement() {
yield console.log('I am incremented');
yield delay(3000);
yield put({ type: 'INCREMENT_FROM_SAGA' });
}
// Task Watcher
export function* incrementSaga() {
yield takeEvery('INCREMENT', onIncrement);
}
takeEvery
takeEvery()でAdd 1ボタンを押したときにdispatchされるAction('INCREMENT')を監視し、TaskとなるonIncrement()を実行します。
takeEvery()では監視しているActionが実行されるたびに、Taskを実行します。
そのため、以下のようにINCREMENTとINCREMENT_FROM_SAGAが同じ回数実行されます。

takeLatest
incrementSagaをyield takeLatest('INCREMENT', onIncrement)に書き換えてAdd 1ボタンを2回押すと、INCREMENT_FROM_SAGAは一回しか実行されません。
このように、takeLatest()では現在実行中のTaskのみを取得し、その前に実行していたTaskをキャンセルします。

参考資料
