AbortController with redux-thunk and redux-saga
Features that allow users to fire multiple requests quickly — where only the latest request is relevant — should include an AbortController. 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 the server and update the search results. As requests for previously selected filters are irrelevant, it makes no sense for the browser to try to resolve them all. On the contrary, it can slow down the application considerably, especially if the request payload is large and/or the Internet connection is slow. This also helps avoid race conditions.
However, it is relatively easy to remedy. The Web API in most browsers gives 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 one.
Here’s 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 one.
let abortController; // Module scoped abort controller
function searchPost(action) {
const { searchFilterParams } = action.payload;
if (abortController) {
abortController.abort(); // Tell the browser to abort the previous 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 call `abortController.abort()` above the request is cancelled
})
.then((data) => dispatch({ type: 'SEARCH_POST_SUCCESS', payload: data }))
.catch((error) => {
if (error.name === 'AbortError') {
return; // Aborting request will throw 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 { takeLatest, call, put, cancelled } from 'redux-saga/effects';
function* searchPost(action) {
const { searchFilterParams } = action.payload;
const abortController = typeof AbortController !== 'undefined' && new AbortController();
try {
const data = yield call(fetch, '/search', {
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 the request
}
}
}
function* watchSearchPost() {
yield takeLatest('SEARCH_POST', searchPost);
}