I recently wrote an article describing my efforts to build a version of Angular's Heroes app using React. In an earlier version of my app, I tried using Redux, since that is often considered simply 'what is done' in React applications. In the course of building my app, I discovered a number of issues concerning Redux. In the end, I removed it altogether and adopted a different solution using RxJS observables. What these issues are and how I came about my solution are the subject of this article.
How Redux works
At the centre of a Redux application is the "store", a plain old Javascript object in which all state is contained. A Redux application only ever has a single store.
Data required by the application is read directly from the store. However, the store cannot be updated directly; Instead, parts of the application fire off "actions" which the store listens to and then decides for itself how (and even if) it wishes to update itself. The mechanism by which this takes place involves functions called "reducers", which take the action and the current state of the store and return the new state of the store.
Race conditions whilst using Redux with React
Redux applications can suffer from race conditions. I have created a small application here which demonstrates the problem. The application displays a list of links which when clicked on result in a panel opening, displaying information on a particular fruit. The data for each panel is fetched from the server (here simulated) when the link is clicked.
Normally, everything works fine; however, if you click between links too fast, you may see some unexpected behaviour: Firstly, click on the banana link. Then, quickly, click on the apple link. You would expect to see the banana panel followed by the apple panel. However, because of the variable time delay whilst the data is in transit between the client and the server, what you may actually see is the apple panel followed by the banana panel! The reason for this is that the time taken to fetch the banana data is longer than the time to fetch the apple data, and the application simply displays whatever data it receives first; It makes no attempt to display the data in the order in which we request it.
It may be thought that this is a bug in my code, but, actually, this is how Redux is designed to work: actions are independent from each other. The action that is dispatched to make the request for data is not directly connected to the action that is dispatched when the response from the server returns.
Is Redux suitable for all Web applications?
We can probably think of solutions to this: For example, we could put a time stamp on actions and then ignore actions that are deemed 'state'; however, I believe that this issue is symptomatic of a deeper problem: that Redux is been given a use-case to which it is not suited.
Redux is described as a 'state-management' solution. But does this application really have any state to manage? On every request, we remove all the old data and replace it with an entirely new set of data. What does this remind us of? Is it not analogous to the web itself, where every HTTP request is unrelated to each other? On reflection, it seems that this is exactly what our app is doing; The only real difference is that the re-rendering of the page is being handled client side and the requests are being made via Ajax.
Using Streams instead of Redux
So perhaps, Redux is not the appropriate solution for use-cases where no state is persisted between Ajax requests. After all, what is the point of a 'store' if data is not really being stored in it?
A more appropriate paradigm to follow for this kind of app are streams. Streams, also known as Observables, are like arrays where the items in the array arrive over time, rather than all at once. Accordingly, they can be used to model a series of connected events. The appropriateness of this for applications which require requests and responses to be connected together should be obvious.
I have built a version of this app using RXJS, an implementation of streams, which fixes the race condition described above.
The stream of navigation events is mapped to request events, which in turn are mapped to response events. Thus, we have an unbroken chain of events. Because we use the switchMap function, whenever a new request event occurs, any pending requests are cancelled. If a user clicks on a new link when the data from an existing request is pending, that older request will be ignored.
We still need to write code to avoid the race condition, but streams make it straightforward and elegant to do so.
Conclusion
Redux should not be the automatic choice when building React SPAs. For applications which do not persist data between Ajax requests, streams are a much better paradigm to follow. RXJS is an implementation of streams that is useful for ordering events within an app.