Frontend Quick Tips #11 A Way of Dealing With Race Conditions in Redux
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 $$$
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