Image for post
Image for post

Handling race conditions with redux-thunk

Reme Le Hane
May 22, 2018 · 4 min read

One very important feature included in many modern web applications is search — our users always need to be able to find something.

Recently I had to hook up a “real-time” search powered by elastic search. As the user types we start returning a short list of what is potentially most relevant to what your typing. To make results more relevant and useful we naturally limited the initial API call to only after the third character was entered and also debounced the async dispatch, so all of this works swimmingly and was easy enough to set up.

This is where potential problems begin to come in, even with a debounce we still have multiple calls that are likely to fire off, and there is no way to control the order they return in. To ensure we have the correct results for the user, all we really care about is the last call that we made.

After some time spent sifting through Google results looking at the various other ideas users had come up with, the idea arose to use timestamps in order to filter out all but the last made call and in turn update the state with the desired result set.

This solution turned out to work pretty well and was relatively easy to set up with only minor changes needed to how you have already structured your actions/thunks and reducer.

Image for post
Image for post

Action

You need to set up a start action that includes a data payload, this will be used in the thunk for setting the time in the reducer for the start of the search query, in addition to success and error:

export const SEARCH_POST_START = 'SEARCH_POST_START';

Thunk

As for the thunk, all you should need to do is update it to include this new action before the API call begins:

export function search(searchQuery) {
return async (dispatch, getState) => {
const timestamp = Date.now();
const startData = { searchText: searchQuery, timestamp };
dispatch(searchPostStart(startData));
try {
const url = `${URL}?searchQuery=${searchQuery}`;
const options = {};
const response = await fetch(url, options);
const json = await response.json();
const success = { searchResults: json, timestamp };
return dispatch(searchPostSuccess(success));
} catch (error) {
const err = { error: true, errorMessage: error };
return dispatch(searchPostError(err));
}
};
}

In the above snippet, you will see at the start of the thunk we dispatch the start action which includes the search query as well as the current timestamp.

The same logic then applies to the success and error, each of them gain this timestamp from this instance of the call for later comparison in the reducer.

Reducer

The final piece happens here, firstly a small helper comes in which does a simple check on the incoming timestamps:

function actionIsValid(state, data) {
return data.timestamp >= state.timestamp;
}

As you can see all it is doing is ensuring that the incoming timestamp is not older than the one stored in the current state:

export const initialState = {
error: false,
errorMessage: undefined,
isSearching: false,
searchResults: [],
searchText: '',
timestamp: undefined,
};

I just included the start and success for brevity, you will see I am simply spreading the data object from the action dispatched from the thunk, which was set up with keys matching those in the reducer for error, searchResults, timestamp and based on the is ationIsValid helper you can determine if the state needs to be updated or not.

For a complete, and better-highlighted example, here is a complete gist.

Image for post
Image for post

Thanks for reading — I hope you found this useful, if so do not forget to clap and share :D


DailyJS

JavaScript news and opinion.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store