Introduction

State management is a concept best described as certain patterns and principles that we follow to make our apps state as convenient to manage as possible. State management libraries provide us with tools that put those patterns and principles into practice. The managing state can get complicated pretty quickly. That is why so many different state management libraries try to solve the same problem in different ways. One problem is using these state management tools, but a whole different and arguably more important problem is deciding if you even need to use one.

React can be summed up as a UI presentation library. Having a fully-fledged out state management tool is not its main focus, so it doesn’t have one. But this doesn’t mean that the tools that React provides aren’t good enough to manage our state. In fact, there is a lot you can do purely by using React's native state management. So before jumping to the conclusion that you need to use a library for this purpose because you’ve heard that state management libraries are the way to go, think about if what you want to do can be accomplished without them and then decide if you really need them. Next, we will talk about situations in which it would be wise to consider using state management libraries.

When should you use state management libraries?

There comes the point during the development process at which managing state becomes cumbersome. This happens most when you are trying to pass down states to components deeply nested in your component hierarchy. You would manually have to pass the state as a prop to each component through the hierarchy until it gets to the one that actually needs the state that you passed around. This is called prop drilling.

Diagram 1

Similar problems might occur when you want to pass states between sibling components. First, you would have to pass a callback function to the component that sends the state, and that callback would only get the data to the parent-most component between the targeted siblings. You would need to pass the data down to the component you want to receive the parent component's passed-around state.

Diagram 2

When you start encountering these problems, you might consider using a state management library such as Redux or MobX. The way these libraries help you interact with your data is by making it globally available. That means that your states no longer need to be passed around hierarchically, but will rather stay in one place, usually called the store, and will be available for access to all of the components inside of your app. Now keep in mind that this doesn’t mean that all of your states should be kept in the store. Component states that are important to that component and that component only should always be kept locally - inside of that component. All the other states that are important to multiple components should be available globally through the store.

Here’s what the previous diagram would look if we were to use a Redux store in our app:

Diagram 3

Data gets sent to the store using a combination of action and reducer functions. From the store, it gets sent to all of the components subscribed to that data. In this way, our components don’t have to worry about sending data to other components and only worry about creating UIs based on that data. Different state management libraries will handle this middleware in different ways.

Which library should you use?

As previously stated, there are many state management libraries to choose from. Each serving the same purpose but in a different way. Most of these libraries are compatible with all of the major frameworks and, of course, vanilla JavaScript, but some of them are framework-specific:

  • React has the Context API that makes dealing with states easier. It is not designed to replace state management libraries, but rather it is a tool designed to help us build our own state management systems;

  • Vue has its own library called Vuex. It is not part of the core Vue library, so you still have to download it separately. The core Vue team maintains it. Of course, like React, Vue has its own low-level built-in ways to manage local component state, but these have the same flaws as previously explained;

  • Angular also has its own library called NgRx. It is basically Redux for Angular. Angular has far less of a need for state management libraries as you can make state management less cumbersome by following good design patterns.

Here we will be focusing on libraries available for React. Some of the most popular ones are Redux, MobX, and Easy Peasy. We will also cover the Context API.

Redux

Redux is probably the most popular state management library. Its hard learning curve and library highly utilizes functional programming concepts and combines them with Flux Architecture. Overall, Redux is intended to make it easier to understand when, where, why, and how data changed in your application. Some of the core principles of Redux are:

  • There is a single source of truth - the store;

  • The state in the store is read-only (immutable);

  • Actions invoke changes to the store;

  • Reducers update state.

Actions are used to describe certain events that happened in your components. They are the only bridge between the store and the application, making them the only source of information for the store. They are simply objects that are returned from pure functions. The action object must contain a type property that indicates the type of action being performed. It also contains any other necessary information which you might want to update the state with. Actions are sent using the store.dispatch(), and the store updates its state using the action object's data through a reducer function.

Reducers are used to specify how the state inside the store changes depending on the incoming action.type. They are pure functions that take the current state and the incoming action.type and return a changed version of the current state. The way the current state is changed is defined by the action.type. Basically, the action tells the reducer a certain event happened and based on that event type, we create a new state.

The store is what brings our actions and reducers together into a single stateful object. We create a store by first combining all of our reducer functions into one object and then using createStore(reducerFunctions). When we use Redux, we create only a single store. From the official documentation, the store only has the following responsibilities:

  • Holds application state;

  • Allows access to state via getState();

  • Allows the state to be updated via dispatch(action);

  • Registers listeners via subscribe(listener);

  • Handles the unregistering of listeners via the function returned by subscribing (listener).

MobX

One of the most important things developers consider when choosing which technology to use is their learning curve. MobX is significantly easier to learn when compared to Redux. That is because MobX applies a more familiar object-oriented paradigm rather than Redux's functional paradigm. It follows a straightforward philosophy:

Anything that can be derived from the application state should be derived. Automatically.

Observable state: MobX is a reactive state management library. That means that our components don’t have to subscribe to any states like in Redux; they are automatically re-rendered when states that they use to get updated. This is made possible by using an observable state. We use observables to give primitive values and references (like objects and arrays) observable capabilities. This means that MobX listens to changes that happen on observer components and properties. Adding observable capabilities can be done by annotating class properties with the @observable decorator like so:

1import { observable } from "mobx"
2
3class Todo {
4    title = ""
5    description = ""
6    @observable tasks = []
7    @observable completed = false
8}

Computed properties: These are basically gettered that return values derived automatically from observable properties. This means that whenever observables change their values, computed properties automatically update and re-render all of the components that depend on them. That means that no observers are needed on components rendering the computed property.

[1] A computed property is like a component that "renders" data: a bounded unit of memoization and re-evaluation. It re-renders automatically when there is a change in any of the observables it depends on.

We create computed properties by annotating them with the @computed decorator:

import { observable } from "mobx" class Todo { title = "" description = "" @observable tasks = [] @observable completed = false @computed get unfinishedTasks = () => this.tasks.filter(task => !task.isFinished) }

In MobX, there are multiple stores. This allows us to separate data with different purposes into different containers. Another important thing to know is that in MobX, we change states directly rather than reducer functions, making our stores mutable.

Context API

The first thing you should know about Context is that it doesn’t have a globally available store that stores our states. The way it handles our data is by using something called a context. Context is basically a wrapper that we use to wrap certain components with, and this makes the data inside the context available to all of the components inside the wrapper. Of course, this doesn’t mean that we couldn’t wrap our whole application inside of one wrapper if we wanted to. Still, the Context API point is to have multiple logically separated contexts so that our components only have access to the data they need. We can create a context by using React.createContext({}).

It requires two different pieces - a Provider and a Consumer. The provider is a component that connects a certain part of our component hierarchy (consumers) so that all of its child components can share the same data stored in our context. So basically, providers represent the bridge between our components and our context. Providers can be nested, and they can also provide data to multiple consumers. The way our components interact with our data is through a property called value. Value is an object that we pass down to our components, and from it, the components pull the data they need. A great thing about this value property is that it allows us to pass down methods or functions used to manipulate the data stored in our context. You are probably familiar with this way of doing things because this is the same way React handles local state management, except now we are managing our state globally.

So the advantage of using the Context API is that upon deploying our app, our bundled code size will be significantly smaller because we don’t have to import any external state management libraries. It is also relatively straightforward to use once you get the hang of it.

[2] Context is ready to be used for low-frequency unlikely updates (like locale/theme).

This means that Context is not yet ready to handle frequent updates to the state but rather updates that rarely happen, like theme changes and user authentication.

Conclusion

We talked about situations in which it would be wise to consider using state management libraries and described the most popular ones for React. The most important thing you should get out of this article is that state management libraries should not be taken lightly. Meaning that before using them, you need to be sure that you actually need them. Your code can get pretty complicated to debug if you start using them. Carefully do your research and then decide if they are necessary for your project.