LoginSignup
1
2

More than 1 year has passed since last update.

【Redux】redux-sagaにおけるtakeEveryとtakeLatestの挙動確認

Posted at

はじめに

新しい現場で状態管理に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を汚さずに済み、テストも書きやすくなります。

image.png
Reactでredux-sagaで非同期通信 〜redux-thunkとの比較も〜

実装

カウントボタンを用意し、Add 1ボタンを押したときの挙動を確かめました。

Viewコンポーネントの作成

結果値、Add 1ボタン、Minus 1ボタンが縦に並ぶような画面を作成します。
mapToPropsの実装がめんどくさいイメージがあったのですが、useSelectoruseDispatchという便利なHooksがあることを知り、これらを使ってみました(とても簡単にかけた...)。

sagas-example.component.jsx
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.jsredux-sagaからインポートしたcreateSagaMiddlewaresagaMiddlewareを作成し、middlewareに登録します(ActionとStateのログを監視するためにredux-loggerも登録しています)。

sagaMiddleware.run(incrementSaga)で後に作成するincrementSagaを実行します。

store.js
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は以下のようになっています。

root-reducer.js
import { combineReducers } from 'redux';

import appReducer from './app.reducer';

const rootReducer = combineReducers({
  app: appReducer
});

export default rootReducer;
app.reducer.js
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()を作成します。

app.saga.js
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を実行します。
そのため、以下のようにINCREMENTINCREMENT_FROM_SAGAが同じ回数実行されます。
スクリーンショット 2021-11-07 14.42.21.png

takeLatest

incrementSagayield takeLatest('INCREMENT', onIncrement)に書き換えてAdd 1ボタンを2回押すと、INCREMENT_FROM_SAGAは一回しか実行されません。
このように、takeLatest()では現在実行中のTaskのみを取得し、その前に実行していたTaskをキャンセルします。
スクリーンショット 2021-11-07 14.47.48.png

参考資料

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2