構成要素
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_PAGE
で isLoadCompletion
を false
にして LOAD_PAGE_SUCCESS
で true
にする
非同期処理を扱いやすくする
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);
https://github.com/hiroppy/ssr-sample/blob/master/src/client/sagas/pages.ts