Frontend Quick Tips #11 A Way of Dealing With Race Conditions in Redux

Photo of Bernard Klatka

Bernard Klatka

Updated Feb 14, 2023 • 5 min read
man focused

No one likes those big articles - that’s why we’re creating Quick Tips - short tips to change your developer's life from the moment you read them.

Those may be some patterns explained in JS code on real-life examples or some techniques for better code.

Problem

You make network requests in your application. You react to those requests in your reducers. Since you have no guarantees about request timing, you also have no guarantees on the order of their completion. So, from time to time it happens that you override the response of a newer request with the response of an older request. Uh oh.

Minimal example - Source

Solution

The solution I came up with is to

  • Track each request being made with an ID and
  • React only to the one with the latest id.

This has the effect of completely ignoring (not cancelling!) any request except the most recent one.

Make a state that contains the id of the latest request (here just a counter):

{

request: number,

// ...

}

and every time you trigger a request, generate a new id (here by incrementing the counter) and attach a new value to the action

const actionCreator = (...) => {

const requestId = getState().request + 1



dispatch({

type: "THING_TYPED",

payload: text,

})



dispatch({

type: "REQUEST_STARTED",

meta: {

requestId

}

})



fakeFetch(text).then(response => {

dispatch({

type: "RESPONSE_RECEIVED",

payload: response,

meta: {

requestId,

}

})

})

}

Then, in the reducer, whenever a request is started, save its id

case "REQUEST_STARTED":

return {

request: action.meta.requestId,

// ...

}

and when receiving a response, discard every request that’s not the most recent one.

case "RESPONSE_RECEIVED":

if (action.meta.requestId !== state.request) {

return state

}

else {

// ...

}

Profit $$$

Source

Multiple requests

Track each one separately.

{

requests: {

users: 2,

produts: 10,

settings: 12,

},

otherState: {

// ...

}

}

Caveats

Ignoring instead of canceling

Bear in mind that this fixes the effect of race conditions on the state of your application, but it doesn’t actually cancel the outdated requests, which might impact the performance of your application, both on the frontend and the backend.

Counter might reach max int

When assigning an id, use (latestId + 1) % Number.MAX_SAFE_INTEGER to make it overflow in a controllable way, or use something like uuid instead.

I didn’t go for uuids because in my app the id is generated inside the reducer. Using uuids would make it impure.

You might not need it

I claim that this works with any async solution for Redux. I’ve shown it on thunks, because that’s the most popular and accessible, but I actually translated it from redux-loop, which I use in my app.

That said, if you’re using more sophisticated tools, like redux-saga or redux-observable, then you probably have something built-in for this problem (takeLatest in saga or switchMap in observable).

Sources

Source for minimal problem example

Source for solution with React and thunks

MDN Docs for MAX_SAFE_INTEGER

uuid on npm

Using switchMap to avoid race conditions

Docs of takeLatest

Photo of Bernard Klatka

More posts by this author

Bernard Klatka

Read more on our Blog

Check out the knowledge base collected and distilled by experienced professionals.

We're Netguru

At Netguru we specialize in designing, building, shipping and scaling beautiful, usable products with blazing-fast efficiency.

Let's talk business