AbortController with redux-thunk and redux-saga
Features that allow the user to fire multiple requests quickly and where only the latest request is relevant, should build in an abort controller. A typical example of this is a search filter allowing the user to quickly select and deselect filters. With each clicked filter the application will fire another request to server and update the search results. As requests for previously selected filters are irrelevant, it makes no sense for the browser trying to resolve them all. On the contrary it can slow down the application considerably, especially if the requests payload is large and/or the Internet connection slow. Not to mention to avoid racing conditions.
It is however relatively easy to remedy. The Web API (in most browsers) give us access to the AbortController
class. The AbortController
class enables the application to send an abort signal to an ongoing request, before starting a new.
An action creator, searchPost.js
that fetches search results. The logic is dead simple, if you call the action creator, it will close any previous requests before attempting a new.
let abortController; // Module scoped abort controller
function searchPost(action) {
const { searchFilterParams } = action.payload;
if (abortController) {
abortController.abort(); // Tell the browser to abort request
}
abortController = typeof 'AbortController' !== 'undefined' && new AbortController();
return (dispatch) => {
dispatch({ type: 'SEARCH_POST' });
return fetch(`/search`, {
method: 'POST',
body: JSON.stringify(searchFilterParams),
signal: abortController.signal, // Passing the abort signal to the request, so when we above call the `abortController.abort()` the request is cancelled
})
.then((data) => dispatch({ type: 'SEARCH_POST_SUCCESS', payload: data }))
.catch((error) => {
if (error.name === 'AbortError') {
return; // Aborting request will return an error we don't want to handle in redux
}
dispatch({ type: 'SEARCH_POST_ERROR', payload: error });
});
};
}
The same can easily be done with redux-saga:
import { takeEvery, call, put } from 'redux-saga/effects';
function* searchPost(action) {
const { searchFilterParams } = action.payload;
const abortController = typeof 'AbortController' !== undefined && new AbortController();
try {
const data = yield call(fetch, url, {
method: 'POST',
body: JSON.stringify(searchFilterParams),
signal: abortController.signal,
});
yield put({ type: 'SEARCH_POST_SUCCESS', payload: data });
} catch (error) {
yield put({ type: 'SEARCH_POST_ERROR', payload: error });
} finally {
if (yield cancelled() && abortController) {
abortController.abort(); // Tell the browser to abort request
}
}
}
function* watchSearchPost() {
yield takeLatest('SEARCH_POST', searchPost);
}