How to use Redux on highly scalable javascript applications?

Redux composition and encapsulation at a large scale

8 min readFeb 22, 2017

--

Edit: As of November 2021, I am not recommending using Redux for sharing API data across React applications. There used to be a time where it was the best way, but today there are amazing libraries out there such as React-Query resolving API state management perfectly well.

Building a large scale application can be problematic, and as it grows it becomes more and more challenging and crucial to keep the structure organized. Redux provides great benefits from having a global application state, but on the other hand, it can become messed up easily and break your app.

I previously demonstrated How to better organize your React applications, where I expose the benefits of a feature-based architecture for React applications. If you require to use Redux, you might wonder how you can use this structure and apply the same type of separation of concerns without the pain.

I have recently worked on the first version of a React-Native mobile app for one of my clients, which desired to architecture the app in a scalable way for a painless experience to add new features later. I believe we should always think about app architecture that way as it can always scale up later on, and I think it is a great idea to show you how I managed it on the Redux side of things with a few examples of code. Be inspired and adapt it to your own need.

Why does it get difficult to maintain my Redux state as my app grows?

Redux is a very great tool and you might want to use it for many reasons. You might need to share some state from a specific component without the need to pass it through props, get data from a server API and share it across your app, keep a history and time-travel over the actions or even store the app state in the local storage of the user’s device to restore it later. But never forget, you might not need redux.

If you decide to use Redux, it’s important to keep the state tree well structured so it can grow as your app grows.

When you have issues with your Redux state, chances are you may have issues maintaining your entire application as well for the same reasons.

  • Do you have too many global action creators?
  • Do you have a huge reducer that manages everything?
  • Do you have conflicts between your actions or reducers that modify the same piece of state?

In most cases, it results from losing control and understanding of your app behaviour, and you end up spending more time debugging than coding. True story.

Add your Redux files with your feature

You can tell when an application has a good architecture when it’s divided into several small features that can be easily extracted and published on a package manager and reused by another application. There is nothing worse than browsing an old project folder directory and not being able to tell if some functions or resources are still in use somewhere in the app because someone forgot to delete them while refactoring some code.

Therefore, you would want to define your Redux actions creators and reducers within the feature that consumes it to keep track of what is used and where it’s used. If you decide you want to remove an entire feature of your app, you can simply unplug it and delete that entire folder which contains the container and presentational components, reducers, actions, resources etc, and you know it will be fine because the state is not coupled to any other feature. Quick, clean and easy.

As your application grows, your features become more complex. To avoid having a single feature that manages too many things, you need to separate them into several smaller features.

A feature must have everything it needs to work on its own. It must also have a limited scope and ideally no awareness of the entire app, but that sometimes is difficult as container components will need to be aware of the shape of the Redux state. Of course, you can nest features into features as long as they stay as much standalone as possible and the nested ones get only used by their direct parent.

Below, Books is a scene, one feature of an app. It has a sub-feature ListItems which is a simple presentational component. The scene also has its own actions and reducers defined in the feature’s folder where the action creators are used by the container component of the feature.

/scenes
/Books
/components
/ListItems
/actions.js
/index.js
/reducer.js

The same way you compose your features into smaller ones, you should also apply this rule to your reducers and divide them, so they are easy to read and maintain. With such a composition, it is preferable to keep the structure of your entire Redux state tree following the folder structure of your features.

Create standalone data modules

Most of the time, container components consume data that are not tightly bound to a specific component and you might wonder where the action creators and reducers are supposed to be defined. A way to do it is to create data modules that take care of individual data objects.

Data module core’s responsibility is to manage a chunk of data of your Redux state. As an example, you can have a module that manages a users object and another one for a books object. You can nest data modules into each other as long as they stay standalone, the same way how features are assembled.

The anatomy of a data module looks like this:

Action creators

They take care of the business logic part. They receive a payload, transform it as needed and return an action that gets dispatched to update the Redux store. In most projects, you might also want to use redux-thunk in order to have asynchronous actions such as sending requests to the server API.

Reducers and Selectors

Reducers handle actions and update the Redux state. There shouldn’t be any logic in your reducers as they need to stay pure. If you tend to have logical code, you should create another action instead.

Selectors are important if you’d like to filter your state before using them in your container components. You can define them in the reducer.js file as they are bound to the same chunk of the state.

API

I recommend you to create a file called api.js where you can define your network requests.

I’ve seen many projects where the only way to send an API request was via a Redux middleware, suggesting that every call will have an impact on the Redux store. But this is not true, you could have API calls that don’t impact the store, such as a simple POST request to send an email, it depends on how you’ve organized your app. But for this reason, I prefer to define them in a separate file.

Once the data module is defined, you just have to import and combine its reducer with the data reducer which is combined itself along with your feature’s main reducer.

Assemble multiple features into one

Now that your features are standalone with their own actions and reducers, you need to assemble them by applying the concept of reducer composition, where a reducer is called by another reducer.

In the example below, there are 2 data modules. The users data module is defined globally, which means any feature can import an action creator from it and use it to make a change in the users sub-tree. The books data module though is nested in the Books scene as it only gets used within the Books feature. In order to have everything working, you need to assemble the main reducer of each child feature into their parent feature’s reducer. You can do that with combineReducers or manually if you have a complex data structure.

For each data folders, we have a reducer in charge of combining all data modules defined in that folder. Even though the reducer might only return a single data module, in the advent of additional data modules being added in the future and to keep everything consistent across the app, it’s better to have them all imported and combined in that single reducer so your feature’s reducer can simply import and use it to make all data modules available at once. The feature’s reducer then gets used by its direct parent’s feature reducer or in the root reducer if it’s defined at the global level.

This is how the reducer in scenes/Books encapsulating the data modules looks like:

In this example, the reducer catches actions from the data module in order to set the loading state whenever there is a request to load new data. You can handle the same action in several reducers or vice versa, mutating what is relevant. This helps you scale your app development because different people on your team might work on different features and data modules handling the same actions without causing merge conflicts while bumping into each other’s code.

Once you’ve properly combined the reducers together for all your features, the Redux state tree of your app should look like this:

Now that your reducers are assembled following this structure, your container components can connect to the Redux store and use the selectors you’ve previously defined to consume the data and dispatch actions of your feature to make changes in the store.

Wrapping up

I hope you’re not afraid anymore when it comes to building highly scalable javascript web or mobile apps with Redux. Regardless how large your app becomes, following such a structure will keep you safe from unexpected things to happen and growing problems.

I published on my Github account a sample project that follows this structure: https://github.com/alexmngn/react-feedback-form

Feel free to add a response below or contact me directly if you have any questions, I’ll be more than happy to help.

More articles from me

About me

Hi, I’m Alexis! I’m a Javascript application developer who has been programming for over 15 years. I specialize in architecting and developing highly scalable web and mobile applications while enjoying functional programming. I recently started to use Twitter, you can follow me here: @alexmngn

--

--

Mobile Application Developer | React-Native enthusiast | Father 👶