#はじめに
新しい現場で状態管理に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をキャンセルします。
#参考資料