Skip to content

Instantly share code, notes, and snippets.

@hiroppy
Last active July 13, 2020 04:44
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hiroppy/9b5daf8da5cd639a62a917d536f5dfc5 to your computer and use it in GitHub Desktop.
Save hiroppy/9b5daf8da5cd639a62a917d536f5dfc5 to your computer and use it in GitHub Desktop.
redux-saga設計

構成要素

1. 一つの責務しか持たない saga

非同期イベントの場合は、success と failure を持ちます。

2. 1 を集めて処理のフローを定義する saga

基本的にこの saga が container からコールされます。(e.g. page のロード等)
ここでは複数の saga が処理順にかかれており、開始から終了までの処理が全て書かれることを想定します。

分ける理由

dispatch(END) を呼び出しやすくする

Server Side Rendering では必ず saga を切るイベントを発行しないといけません。

以下のコードは store の更新が不安定なので間違えです。
これは renderToStaticMarkup の終了に依存して closeが発火されるため実際に saga の処理が終了したとは限りません。

// server.ts
function render() {
  store
    .runSaga(rootSaga)
    .done.then(() => {
      const preloadedState = JSON.stringify(store.getState());

      res.send(renderFullPage({ preloadedState }));
    })
    .catch((e: Error) => {
      res.status(500).send(e.message);
    });

  // kick redux-saga
  renderToStaticMarkup(sheet.collectStyles(jsx));

  // close redux-saga(because using `fork`)
  store.close();
}

// configureStore.ts
export const configureStore = (preloadedState = {}) => {
  const enhancer = createEnhancer();
  const store: Store & {
    runSaga: SagaMiddleware<typeof rootSaga>['run'];
    close: () => void;
  } = createStore(rootReducer, preloadedState, enhancer);

  sagaMiddleware.run(rootSaga);

  store.runSaga = sagaMiddleware.run;
  store.close = () => { // server.tsで呼ばれ、ここでENDを発火しsagaを止める
    store.dispatch(END);
  };
};

以下が正解となります。

// saga.ts
function* loadTopPage() {
  try {
    yield put(fetchOwners());
    yield take('FETCH_OWNERS_SUCCESS');
    yield put(loadTopPageSuccess());
  } catch (e) {
    yield put(loadTopPageFailure(e));
  } finally {
    if (!process.env.BROWSER) yield put(END);
  }
}

END そのページで行われるべきことの一連の流れがすべて終わった時に呼ぶべきです。
つまり、何かしらの全体フローを制御する部分が必要となり、それを 2 で実現することが可能です。

可読性を上げる

2 のようにフローを列挙することにより処理の流れがわかりやすくなります。
1 はあくまでも責務が一つしかないため、汎用的であり他のイベントをtake, put等をするべきではありません。

2を作ることによりSPA時にページのロードの指標を作りやすい

SPAはページ構成と情報取得が別であるため、ローディングが必要となります。
全体のローディングが完了するstateが必要という状況は多くあります。
2でページ全体のフローを構成することにより、全体の開始と終了の指標を作ることが可能です。
e.g. LOAD_PAGEisLoadCompletionfalse にして LOAD_PAGE_SUCCESStrue にする

非同期処理を扱いやすくする

2 のようにフロー内で非同期処理を行うことにより、他の処理の汎用を高めます。 container では非同期処理ができないため、処理の拡張が他の処理に依存する時に大変となります。
分けない場合、後々設計で苦しむと思う。(経験談)

e.g. 最初は一つの API を叩くだけだったので、container で dispatch した。
しかし、新しい API を追加したくなったのだが、最初の API の情報に依存しないといけない場合。

// container.ts
const mapDispatchToProps = (dispatch: Dispatch) => ({
  getUsers: () => {
    dispatch(getIdLists());
    // dispatch(getUserNames()); // もし前のgetIdListsの情報をgetUserNamesで使いたい場合この書き方はできない
  }
});

もしこのように getIdLists と書いてしまった場合、getIdLists で呼び出される saga 内で以下のようになります。

function* getUserNames() {
  try {
    const { ids }: GetIdListsResponse = (yield client('/api/ids')).data; // or actionを発行してもいい
    // もし上のがactionの場合はここでtakeする
    // const { ids } = yield take('GET_ID_LISTS_SUCCESS');
    yield put({ type: 'GET_USERS_NAME', ids });
    const { names } = yield take('GET_USERS_NAME_SUCCESS');
    yield put(getUserNamesSuccess(names));
  } catch (e) {
    yield put(getUserNamesFailure(e));
  }
}

このような書き方にしてしまった場合、もし ID の API だけを叩きたい場合、 saga を別途作らないといけなくなります。

なので 2 のようにもう一回層挟むことにより、単体のsagaの汎用性を高めます。

// container.ts
const mapDispatchToProps = (dispatch: Dispatch) => ({
  load: () => {
    dispatch(loadPage());
  }
});

// saga.ts

// 1
function* getIdLists() {
  try {
    const { ids }: GetIdListsResponse = (yield client('/api/ids')).data;

    yield put(getIdListsSuccess(res));
  } catch (e) {
    yield put(getIdListsFailure(e));
  }
}

// 1
function* getUserNames(ids: GetIdListsResponse['ids']) {
  try {
    const { names }: GetUserNamesResponse = (yield client('/api/names', { body: ids})).data;

    yield put(getIdListsSuccess(names));
  } catch (e) {
    yield put(getIdListsFailure(e));
  }
}

// 2]
// ここで様々なsagaを組み合わせて処理系を設計していく
function* loadPage() {
  try {
    yield put({ type: 'GET_ID_LISTS'});
    const ids = yield take('GET_ID_LISTS_SUCCESS');
    yield put({ type: 'GET_USER_NAMES', ids });
    yield take('GET_USER_NAMES_SUCCESS');
    yield put({ type: 'LOAD_PAGE_SUCCESS' });
  } catch(e) {
    yield put({ type: 'LOAD_PAGE_FAILURE', error: e });
  } finally {
    if (!process.env.BROWSER) yield put(END);
  }
}

yield takeLatest('LOAD_PAGE', loadPage);
yield takeLatest('GET_ID_LISTS', getIdLists);
yield takeLatest('GET_USER_NAMES', getUserNames);
@hiroppy
Copy link
Author

hiroppy commented Aug 29, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment