Notes on React/Redux Application Architecture

Today a friend asked me to lay out what I know about building applications with React and Redux. Since I’m taking the time to write these ideas out anyway, I decided to publish them here so that they would do the most good for the largest number of people. The architecting of React/Redux applications is hard, and since there is so much flexibility in how you can put the elements together, it’s easy to get lost in the mix of examining your many choices and tradeoffs. Hopefully I can clear the path a bit so you don’t feel so lost in the weeds.

Why React? Why Redux?

The reason you use any tool is to get work done, so I want to spend a bit of time justifying the use of React and Redux in building applications.

I want to motivate this discussion by talking about my experience in writing single page web applications. I’ve architected and built applications in jQuery, Backbone, Angular 1, and React. While I found both jQuery and Backbone to be great for the initial development of applications, I noticed that as the applications grew just a little in size and complexity, they became hard to maintain; the abstractions that those frameworks provided were just not powerful enough, on their own, for big applications.

This all changed when I began working at the now defunct startup Marketwiise. I took the job because it was an opportunity to design and build a full stack application using Node, Express, and Angular. I was still new to those technologies when I started, but I took to them quickly and found that I really enjoyed them, especially Angular, with its built in promises, dependency injection, module system, and two way data binding. It felt like a huge leap forward at the time.

But as the web app grew in complexity, just as I had experienced with the more primitive frameworks, it became hard to maintain. The very two-way data binding that made writing the application so easy in the beginning soon proved problematic. Long chains of data binding throughout the application meant that I didn’t really understand how I was modeling or moving my data around. And if you don’t understand how your data is structured, it’s hard to understand and predict what your application is doing.

And if this degenerate data model issue wasn’t bad enough, the god damned component system was way over complicated. So called directives could be custom HTML element tags, element attributes, class names, and even comments! This confusing array of choices was only made worse by the fact that the directive api itself was a beast of complexity.

React and Redux to the Rescue

React and Redux solve these issues. First, React provides a much simpler component and templating system. Since it’s easier to understand once you get the hang of it, it’s easier to write complex, maintainable applications.

However, because React components can store state, you can run into the same problem I had in Angular. When you have enough components storing state and passing it along to other components through various means of communication, you can end up with a very unpredictable chain of data.

And this is where Redux comes in. Redux exists to hold all of our data in a single data model. This data flows out to React components, causing them to render content that users can interact with. And, if a user interacts with our application, or if our application receives data or a message, we can trigger an action, which is dispatched by the Redux store, causing the data model to change, which in turn causes our application to re-render.

This is so important that I really want to beat this into your heads: 1) the point of React is to simplify the building of components and templating; 2) the point of Redux is facilitate a singular, predictable flow of data from the application, to a single, unified data model, and back in a loop to the application. This is about simplicity and predictability.

The Elements of a Successful React/Redux Application

I may write in further detail on these topics at a later date, but for now I just want sketch out the foundation of a successful React/Redux application.

Build Tools

I like to write bleeding edge Javascript, so I always use Babel. I love generators, promises, and many of the other features that are starting to come into standardization.

And while I personally feel like I can get a lot done with just Webpack and npm, I have used Browserify, Cake, Grunt, and Gulp in the past. Also, Rollup and System.js have caught my eye, but I have not played with them yet.

The Redux Store

Redux should be used for storing the global state, and it’s best to normalize your Redux store as much as possible, because this is the data model for your app, and in order to understand the behavior of your application, you need this data to be clean and organized.

And when it comes to your store, there is one major tradeoff I want to call your attention to: plain Javascript objects versus immutable data structures.

The main advantage of Javascript objects here is that you’ll already have an intuitive sense of how to operate on your store. You can use the same Javascript operations that you’ve used in all the apps you’ve built in the past. But this convenience comes with a cost. Because Javascript objects rely on references, there is a danger that your app will share object references with the store, making it easy for React components to modify application state without dispatching Redux actions. This can be mitigated by using things like Object.assign() to make shallow copies of Redux store objects, but you’ll never eliminate the problem completely unless you make deep copies, and that can be very expensive.

Now, if you’re willing to step outside of your comfort zone, I would recommend using an immutable data structure library to implement your store. This will require a learning curve up front, and you will feel like it’s slowing down your development process, but there are huge gains. Immutable data structures are hard to modify outside of your store, and dispatching actions becomes less expensive because you can update the data without copying the whole data structure. I like using Immutable.js for this purpose.

react-redux and reselect

The next big pillar of React/Redux architecture involves how you push the data from your store out into your React components. My first attempts at this involved dumping my whole store into the props of my root component, and then using that root component to set properties on components all the way down the component tree. This is bad for a number of reasons. The first big problem with this is that it makes it easy for your components to be dependent on the structure of your Redux store, and such tight coupling quickly becomes a maintainability issue. And, in addition to the tight coupling, your components will take on a lot of code for computing values from state, obscuring the presentation logic with too much off-the-cuff data transformation code.

You can avoid this by using reselect. reselect allows you to create memoizing selector functions that compute derived data from your store. This helps you to limit the amount of data transformation in your components, it decouples your data model from your component architecture, and it increases efficiency because derived data is only recalculated if data in your store changes.

In addition to reselect, I recommend using react-redux to inject application state into your components. react-redux has a component called Provider that injects the store into your app through your root component using the React context api. You can then use the connect() function to inject properties into your components. With connect(), you can use selectors to compute and inject state with the mapStateToProps parameter, and you can also inject auto-dispatching action creator functions with the mapDispatchToProps parameter. This enables you to decouple your React components from Redux, because it allows your components to be blissfully unaware of store.getState() and store.dispatch(). But what’s even better about this arrangement is that connected components, by design, will receive fewer properties (ideally zero props) from their parents, making it easier to modify and maintain your component render() methods.

Client-Side Routing

I don’t have a lot to say here. I’ve used react-router in the past. It seems fine to me.

Asynchronous Operations (HTTP, AJAX, etc)

Another huge issue in React/Redux applications is deciding where to put the logic for sending and receiving data. Your AJAX calls and websocket code need to go somewhere. When I first started building React/Redux applications, I actually put a lot of this code into my reducers. This, however, was a horrible choice: reducers should just update the Redux store.

I have found that the best way to handle this is to take advantage of Redux middleware. In my opinion, your best options are redux-thunk, redux-promise-middleware, and redux-saga. I’ve used both redux-thunk and redux-promise-middleware, and out of the two, I prefer the promise middleware. From my experience, while it is easy to get up and running with thunks, they get messy when you start to want to sequence lots of asynchronous operations. And, since I’ve started using the promise middleware at my current gig, I find writing complex React/Redux apps to be a much more pleasant experience.

On a final note, I want to make it clear that, while I have yet to use redux-saga in an application, I think it looks awesome, but it strikes me as both very fine grained and a bit on the complicated side, so I would recommend only using it if you really need it.

For further details on this topic, read this article that from Nader Dabit.

Conclusion

There is a lot to architecting and maintaining React/Redux applications successfully, and I want to make it clear that what I’ve written here is far from exhaustive, so make sure you do your homework and try things out. That being said, what I have done here is to give some insight into the most crucial design decisions one needs to consider when building applications with React and Redux. Hopefully this saves you some time and suffering.

Cheers, Paul Rene Nichols.

--

--