Skip to content

Instantly share code, notes, and snippets.

@josepot
Last active December 4, 2016 16:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save josepot/cf63578fa81c7dba89ba156e71274537 to your computer and use it in GitHub Desktop.
Save josepot/cf63578fa81c7dba89ba156e71274537 to your computer and use it in GitHub Desktop.
Sagas: Request Sequence pattern
// Request sequence pattern
// One of the reasons why I love Redux-Saga is because it allows me
// to handle all the common requests in a consistent manner,
// which reduces the amount of boilerplate and helps me keep things
// DRYer. I usually use a variation of the following utility Saga
// whenever I have to make a normal/promise request:
import {
call,
cancelled,
put,
} from 'redux-saga/effects';
import {
onRequestCancelled,
onREquestErrored,
onRequestStart,
onRequestSucceeded,
} from '../actions/creators';
function* requestSequence(originalAction, ...apiCall) {
yield put(onRequestStart(originalAction));
try {
const payload = yield call(...apiCall);
return yield put(onRequestSucceeded(originalAction, payload));
} catch(e) {
console.log('A request errored');
console.log(e.message);
console.log(e.stack);
return yield put(onRequestErrored(originalAction, e));
} finally {
if (yield cancelled()) {
return yield put(onRequestCancelled(originalAction));
}
}
}
// Lets see an easy example on how to use it. Lets imagine that
// we want to allow the user to refresh certain content
// whenever they hit a button. Now, let's imagine that button
// is always enabled, even while there is an ongoing request.
// If the user hits the button again we want to ignore the original
// request and start another sequence of requests.
// So the desired flow of actions for would be:
// REFRESH_PAGE_REQUESTED
// REQUEST_STARTED (as a result of the previous request)
// REFRESH_PAGE_REQUESTED (the user hits the button again)
// REQUEST_CANCELLED (an action that let's us know that ongoing request got cancelled)
// REQUEST_STARTED (an action that's telling us that a new request has started)
// REQUEST_COMPLETED/REQUEST_ERRORED (depending on what happened with the promise).
// This is how we could accomplish that flow:
import { takeLatest } from 'redux-saga';
import { call } from 'redux-saga/effects';
import refreshPage from '../api/refreshPage';
import requestSequence from './utils/request-sequence';
import { REFRESH_PAGE_REQUESTED } from '../actions/types';
export function* refreshSubroutine(originalAction) {
// `requestPage` is a function that retunrs a promise
// with the results of the refresh.
yield call(requestSequence(originalAction, refreshPage));
}
export function* refreshWatcher() {
yield takeLatest(REFRESH_PAGE_REQUESTED, refreshSubroutine);
}
// The thing about this is that if there was an ongoing request
// for `refreshPage` and a new refresh gets triggered, then
// the ongoing task will get cancelled: which means that the
// `REQUEST_CANCELLED` action will get triggered automatically
// before the next request starts. So, we are given the chance
// to clean the store from any traces related with the request that
// it's being cancelled.
// Lets see a more complex example of how to use it.
// Let's imagine that we have an infinite scroll
// list and that we need
// load more items when we are reaching the end. At the same time
// the user can decide to refresh the list at
// any given moment.
// If the user decides to refresh the list we need to cancel (stop listening)
// any ongoing requests that we we had for the nextPage.
// So, the effects of the "refresh" action take priority, they cancel
// the "nextPage" effects. At the same time, while we are refreshing
// the list we should ignore any nextPage actions that could happen.
// We could accomplish that doing this:
// * `loadPage` is a function that returns a Promise with the items of a page.
// For the sake of this example we will assume that if it receives no
// parameters it will perform a hard refresh, otherwise we need to
// provide a pagination token.
function* nextPageListWatcher() {
const action = yield take(LIST_NEXT_PAGE_REQUESTED);
yield call(
requestSequence(action, loadPage, action.payload.paginationToken)
);
}
export function* listSaga() {
while (1) {
const { refreshAction, nextPageCall } = yield race({
refreshAction: take(LIST_REFRESH_REQUESTED),
nextPageCall: call(nextPageListWatcher),
});
if (refreshAction) {
yield call(
requestSequence(refresh, loadPage)
);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment