This blog is about when you should start thinking about using Redux, the problems it solved for us and the benefits we found. It is based on what we learnt from scaling up our React app.
If you don’t know what state is, or you haven’t heard about Redux the rest of this blog might not make much sense. Here is a quick overview if these concepts are new to you. Otherwise skip ahead to the main story.
What is state?
State is just data. Think of it as a collection of application data like the current user’s name, if the user is logged in, if the page is loading. The state for an application might change based on things such as user actions or a response from an API. An application will read the state to determine what sort of User Interface it should show.
What is Redux?
Redux is a tool for managing application state. It’s a very common tool used in React applications.
Why we decided to use Redux
Premature optimisation is the root of all evil — Donald Knuth
This is a quote I follow when I am building software. It’s the main reason why when we started building the latest React app for the Australian Broadcasting Corporation we chose to start off managing our application state using local component state.
This worked really well for the first few weeks but we soon started to see issues with our state management as we added more features to our app.
It became really difficult to trace state changes through our app. The functions that changed our application state were scattered through several React components. Some of our components were becoming bloated with functions to manage our state. It was all getting a bit messy and we saw our codebase slowly starting to resemble a big bowl of spaghetti.
So let me step you through my experience of scaling up our app.
We started out using React local component state.
React has ‘unidirectional data flow.’ This means components send state downwards as props.
We set our state in our top level component then send the data down as props. Easy.
We add some more functionality. Unfortunately, some of our components need to share state, but they don’t share a parent-child relationship.
No worries, we solve this issue by ‘Lifting state’. This means that we lift the state (and functions that change this state) to the closest ancestor(Container Component). We bind the functions to the Container Component and pass them downwards as props. This means that child components can trigger state changes in their parent components, which will update all other components in the tree. Nice.
We’ve added a few features and our application state flow started to look like this…
As you can see the way state is being updated and dispersed across our application is becoming more complex. Here are the main pain points we started to feel as we scaled our application up:
- Our component structure was based on the applications UI. Our applications state did not necessarily follow our UI.
- The shape of our application state was spread across many components.
- The functions that changed our state were spread across many components.
- To have an overview of the application state, you need to have a mental model of the application structure.
- We were passing the same props through multiple levels of components.
- It became harder to trace state changes when debugging our app.
If you are starting to run into some of the above issues, it may mean you are ready for Redux.
How does Redux work?
Let me start by saying not all React applications need to use Redux. In fact most really simple React apps don’t benefit from Redux at all.
The most common complaint about Redux is that it’s quite verbose and can take some time to get your head around. It requires developers to explicitly describe how application state is updated through actions and reducers. It allows you to maintain the application state as a single global store, rather than in local component state.
Here is a quick overview of how Redux is structured.
An action is just information about an event. When you want to update your state with Redux, you always start by dispatching an action. Actions must include the name of the action and optionally any information that needs to be sent with the action. The reason actions must state their type is so the reducer can identify them. In short, an action is just static information about the event that initiates a state change.
When an action is dispatched it is sent to the reducer. The reducer is a pure function that describes how each action updates the store. A Pure function is a function that given the same input, will always return the same output. A key point to also remember is that the reducer will always return a new application state. It will never directly mutate the store.
The store is the application state stored as objects. React components can subscribe to the store and whenever the store is updated, it will update the components.
Here is what that looks like. I’ve added an updated version of the diagram, but this time using Redux.
Remember: With Redux we dispatch an action, which triggers our Reducer function, which updates our Store.
What we learnt
After we updated to Redux, these were the benefits we noticed:
Simplified our React components
- We were able to flatten our React component structure and even remove some container components. This was because we no longer needed to organise the hierarchy of components around passing data downwards.
- We were able to convert some class components to functional components.
Separation of concerns
- Having a distinct section of our application that described all of the actions and reducer functions made it easier for new developers to get a quick overview of the business logic of our app in a single place.
- Our React components became less bloated with state and functions that updated state. This meant our components could just focus on managing the User Interface and were much more readable.
- It was much easier to debug our state, especially with Redux logger.
- Redux is ridiculously easy to write unit tests for.
Premature optimisation is the root of all evil — Donald Knuth
While I still hold true to this quote, I believe that implementing Redux in most React applications early on is the right approach. From our experience, the point where just using local state becomes cumbersome happens pretty quickly.
If your app meets some of the following criteria, I reckon implementing Redux right away is the best approach.
- The UI can vary significantly based on application state.
- State doesn’t always flow in a linear, unidirectional way.
- Common user journeys through your app involve multiple state updates.
- Many unrelated components update state in the same way.
- The state tree is not simple.
- State is updated in many different ways.
- You need to be able to undo previous user actions.
It is worth noting that we still do use local state in our app for certain components. For example we have quite a few form screen components and we feel that handling form validation is still best done using local state for now. There are a few reasons why we kept local state for our form screens.
- We didn’t want the form state to persist after the component dismounted.
- Form validation state was never shared outside of the component.
Want to get started?
Here are some of the fantastic resources I have used to learn Redux.
- Getting started with Redux from egghead.io
- Redux documentation
- Levelling up with React: Redux from CSS Tricks
Thanks for reading!