Last active
December 4, 2016 16:51
-
-
Save josepot/cf63578fa81c7dba89ba156e71274537 to your computer and use it in GitHub Desktop.
Sagas: Request Sequence pattern
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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