redux-sagaを理解できない理由と使い方


Reduxの非同期処理を書くとき実装のしやすさからredux-thunkを選ぶことがありますが、大規模な開発になるとredux-sagaの方が良いのでその実装方法について書きます。

これから書くことはほぼUdemyの講座「Redux Saga (with React and Redux): Fast-track intro course」の内容を元にしています。

redux-thunkについてはこちらでまとめています。
Redux Thunkでactionに非同期処理を書く

redux-sagaをなかなか理解できない理由

reduxの非同期処理でredux-sagaを使おうと導入したら挫折した経験はありませんか?

SampleSaga
function* watchGetUsersRequest() {
  yield takeEvery(actions.Types.GET_USERS_REQUEST, getUsers);
}

function関数の末尾の*。「このアスタリスクは何だ?」と思いませんでしたか?
またyieldについても「なんだこれは??」と。

これはECMAScript6から使えるようになったgenerator関数の書き方になります。
そのgeneratorは戻り値としてiteratorを返します。
iteratorもECMAScript6から使えるようになりました。

generatorについて

  • 関数になります。functionの末尾に*を使って宣言します
  • 関数の中でyieldを使います
  • iteratorのnext関数を使って処理を行います
generatorの例
function* hoge() {
  yield 1;
}

let iter = hoge();
iter.next(); // { done: false, value: 1 } valueの「1」はyieldで指定した「1」です
iter.next(); // { done: true, value: undefined } 呼び出すデータがなくなったのでdoneがtrueになっています
  • yieldは何回も使うことができます
  • next関数を使うとyieldで処理が止まります
generatorの例
function* hoge() {
  console.log('');
  console.log('');
  yield 1;
  console.log('');
  yield 2;
  console.log('');
  console.log('');
  console.log('');
  yield 3;
}

let iter = hoge();
iter.next();
/*
"い"
"ろ"
{
  done: false,
  value: 1
}
*/

iter.next();
/*
"は"
{
  done: false,
  value: 2
}
*/

iter.next();
/*
"に"
"ほ"
"へ"
{
  done: false,
  value: 3
}
*/

iter.next();
/*
{
  done: true,
  value: undefined
}
*/

redux-sagaはgenerator関数を使ってyieldごとに処理が行われることが分かります。

effectsの説明

redux-sagaには非同期処理を行う上で使用する関数があります。
それらを理解するために処理について説明します。

  1. takeEvery
  2. takeLatest
  3. take
  4. call
  5. put

takeEvery

  • actionがdispatchされる度に監視させたい処理
  • 例) APIからデータのリストを取得するとき
function* watchGetUsersRequest(){
  yield takeEvery(action.Types.GET_USERS_REQUEST, getUsers);
}

takeLatest

  • actionが複数回dispatchされる可能性があるとき、現在実行中の最新のsagaのみを取得する処理
  • 例) レコードの作成または更新
  • 例) 同時に複数のコンポーネントから同じAPIエンドポイントを参照するとき
function* watchCreateUserRequest() {
  yield takeLatest(actions.Types.CREATE_USER_REQUEST, createUser);
}

take

  • 実行中のsagaが完了するまで、そのactionがdispatchされるタイミングを監視するとき
  • 例) ユーザーの削除
  • 例) 進行中の処理が完了するのを待ってから、別の処理を行うとき
function* watchDeleteUserRequest() {
  const action = yield take(actions.Types.DELETE_USER_REQUEST);
  yield call(deleteUser, {
    userId: action.payload.userId,
  });
}

call

  • 関数またはPromiseを呼び出すとき、その関数またはPromiseの実行が完了するのを待ってから次のコード行を実行するとき
  • Promiseの完了を待つ
function* deleteUser({ userId }){
  try {
    const result = yield call(api.deleteUser, userId);
  } catch(e) {}
}

function* watchDeleteUserRequest(){
  while(true){
    const { userId } = yield take(action.Types.DELETE_USER_REQUEST);
    yield call(deleteUser, { userId });
  }
}

put

  • actionをdispatchするとき
  • 例) APIから受け取ったdataで更新するとき
  • 例) error処理を行うとき
function* getUsers() {
  try {
    const result = yield call(api.getUsers);
    yield put(actions.getUsersSuccess({
      users: result.data.users
    }));
  } catch (e) {
      yield put(actions.usersError({
        error: 'An error occurred when trying to get the users',
      }));
    }
  }
}

Set Up

redux-sagaはreduxのmiddlewareになるのでcreateStoreに設定する必要があります。

import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootSaga from './sagas';

const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducers, applyMiddleware(sagaMiddleware));

sagaMiddleware.run(rootSaga);

rootSagaの設定

  • rootSagaでsagaの処理を配列で管理する。allはPromiseAllと同様の処理
sagas/index.jsx
import { all } from 'redux-saga/effects';
import usersSagas from './users';

export default function* rootSaga() {
  yield all([
    ...usersSagas,
  ]);
};
  • 各非同期処理をforkでアタッチさせる。(この説明は合ってるのか。。?)
sagas/users.jsx
import { fork } from 'redux-saga/effects';

const usersSagas = [
  fork(watchGetUsersRequest),
  fork(watchCreateUserRequest),
  fork(watchDeleteUserRequest),
];

export default usersSagas;

CRUD処理を書いてみる

  • ユーザーのIDと名前を管理してます
  • errorハンドリングしています
  • createとdelete処理を行った後、yield call(getUsers)でユーザー一覧を更新してます

共通処理

action

action/users.js
export const Types = {
  GET_USERS_REQUEST: 'users/get_users_request',
  GET_USERS_SUCCESS: 'users/get_users_success',
  CREATE_USER_REQUEST: 'users/create_user_request',
  DELETE_USER_REQUEST: 'users/delete_user_request',
  USERS_ERROR: 'users/user_error',
};

export const getUsersRequest = () => ({
  type: Types.GET_USERS_REQUEST,
});

export const getUsersSuccess = ({ items }) => ({
  type: Types.GET_USERS_SUCCESS,
  payload: {
    items,
  },
});

export const createUserRequest = ({ name }) => ({
  type: Types.CREATE_USER_REQUEST,
  payload: {
    name,
  },
});

export const deleteUserRequest = (userId) => ({
  type: Types.DELETE_USER_REQUEST,
  payload: {
    userId,
  },
});

export const usersError = ({ error }) => ({
  type: Types.USERS_ERROR,
  payload: {
    error,
  },
});

api

api/users.js
import axios from 'axios';

export const getUsers = () => {
  return axios.get('/users');
};

export const createUser = ({ mame }) => {
  return axios.post('/users', {
    name,
  });
};

export const deleteUser = (userId) => {
  return axios.delete(`/users/${userId}`);
};

reducers

reducers/users.js
import { Types } from '../actions/users';

const INITIAL_STATE = {
  items: [],
  error: '',
};

export default function users (state = INITIAL_STATE, action) {
  switch(action.type) {
    case Types.GET_USERS_SUCCESS: {
      return {
        ...state,
        items: action.payload.items,
      };
    }
    case Types.USERS_ERROR: {
      return {
        ...state,
        error: action.payload.error,
      }
    }
    default: {
      return state;
    }
  }
}

Sagaの処理

read

sagas/users.js
function* getUsers() {
  try {
    const result = yield call(api.getUsers);
    yield put(actions.getUsersSuccess({
      items: result.data.data,
    }));
  }
  catch (e) {
    yield put(actions.usersError({
      error: 'An error occurred when trying to get the users',
    }));
  }
}

function* watchGetUsersRequest() {
  // actionの処理をしてからgenerator関数のgetUsersを行う
  yield takeEvery(actions.Types.GET_USERS_REQUEST, getUsers);
}

create

sagas/users.js
// payloadはaction、createUserRequestから取得
function* createUser({ payload }) {
  try {
    yield call(api.createUser, {
      name: payload.name,
    });
    yield call(getUsers); // ユーザー一覧を更新
  } catch (e) {
    yield put(actions.usersError({
      error: 'An error occurred when trying to create the user',
    }));
  }
}

function* watchCreateUserRequest() {
  // actionの処理をしてからgenerator関数のcreateUserを行う
  yield takeLatest(actions.Types.CREATE_USER_REQUEST, createUser);
}

delete

sagas/users.js
function* deleteUser({ userId }) {
  try {
    yield call(api.deleteUser, userId);
    yield call(getUsers); // ユーザー一覧を更新
  } catch (e) {
    yield put(actions.usersError({
      error: 'An error occurred when trying to delete the user',
    }));
  }
}

function* watchDeleteUserRequest() {
  const action = yield take(actions.Types.DELETE_USER_REQUEST);
  yield call(deleteUser, {
    userId: action.payload.userId,
  });
}

以上になります。

参考サイト