Web Front-end
9 minute read

The Best React State Management Tools for Enterprise Applications

Teimur Gasanov
Teimur is a React mentor and a Senior Front-end Engineer on the Toptal core team.

Developers of enterprise-level React applications know how crucial state management is to a coherent end-user experience.

However, the user is not the only one affected by state management. React developers create and maintain state. They want state management to be simple, extendible, and atomic. React has moved in this direction by introducing hooks.

Problems may arise when the state should be shared among many components. Engineers have to find tools and libraries that suit their needs, yet at the same time meet high standards needed for enterprise-grade apps.

In this article, I will analyze and compare the most popular libraries and pick the most appropriate one for state management in an enterprise-level application.

Built-in React State Management Capabilities

React has an excellent tool for providing data across multiple components. The primary goal of Context is to avoid prop-drilling. Our goal is to get an easy-to-use tool to manage the state in various scenarios likely to be encountered in enterprise applications: frequent updates, redesigns, the introduction of new features, and so on.

While all this is theoretically doable with Context, it would require a custom solution that requires time to set up, support, and optimize. The only advantage of Context is that it doesn’t depend on a third-party library, but that can’t outweigh the effort to maintain this approach.

In addition, React team member Sebastian Markbage has mentioned that the new Context API was not built and optimized for high-frequency updates but rather for low-frequency updateslike theme updates and authentication management.

Examining Existing Libraries

There are dozens of state management tools on GitHub (e.g., Redux, MobX, Akita, Recoil, and Zustand). However, taking each of them into consideration would lead to endless research and comparisons. That’s why I narrowed down my selection to the three main competitors based on their popularity, usage, and maintainer.

To make the comparison explicit, I’ll use the following quality attributes:

  • Usability
  • Maintainability
  • Performance
  • Testability
  • Scalability (works with the same performance on the bigger states)
  • Modifiability
  • Reusability
  • Ecosystem (has a variety of helper tools to extend the functionality)
  • Community (has a lot of users and their questions are answered on the web)
  • Portability (can be used with libraries/frameworks other than React)

Redux

Redux is a state container created in 2015. It became wildly popular because:

  • There was no serious alternative when it launched.
  • It provided separation between state and actions.
  • react-redux magic enabled straightforward state connection.
  • The co-creator of the library is acclaimed Facebook developer and React core team member Dan Abramov.

Animation showing the progression of states and actions from and to the reducer, using Redux.

You have a global store where your data lives. Whenever you need to update the store, you dispatch an action that goes to the reducer. Depending on the action type, the reducer updates the state in an immutable way.

To use Redux with React, you’ll need to subscribe the components to the store updates via react-redux.

Redux API Example

The fundamental parts of Redux in the codebase that differentiates it from the other tools are slices. They contain all the logic of actions and reducers.

CodeSandbox

// slices/counter.js
import { createSlice } from "@reduxjs/toolkit";

export const slice = createSlice({
  name: "counter",
  initialState: {
    value: 0
  },
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    }
  }
});

export const actions = slice.actions;
export const reducer = slice.reducer;


// store.js
import { configureStore } from "@reduxjs/toolkit";
import { reducer as counterReducer } from "./slices/counter";

export default configureStore({
  reducer: {
    counter: counterReducer
  }
});


// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import App from './App'
import store from './store'

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)


// App.js
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { actions } from "./slices/counter";

const App = () => {
  const count = useSelector((state) => state.counter.value);
  const dispatch = useDispatch();

  return (
    <div>
      <div>
        <button onClick={() => dispatch(actions.increment())}>Increment</button>
        <span>{count}</span>
        <button onClick={() => dispatch(actions.decrement())}>Decrement</button>
      </div>
    </div>
  );
};

export default App;

Quality Attributes

  • Usability. Redux became very simple with the introduction of the official toolkit package. You create a slice (a combination of the initial state, reducers, and actions), pass it to the store, and access it in a component via hooks.
  • Maintainability. Redux is simple. It doesn’t require deep knowledge to understand how to enhance or repair something.
  • Performance. The primary performance influencer with Redux is the software engineer. Redux is a straightforward tool without much logic. If you see that state updates are slow, you can follow the official guidelines to make them faster.
  • Testability. Redux consists of pure functions (actions and reducers), making it great for unit testing. It also provides the mechanism to write integration tests where the store, actions, and reducers work together.
  • Scalability. By default, Redux has one global state, making it hard to scale. However, there is a redux-dynamic-modules library that enables creation of modular reducers and middleware.
  • Modifiability. Customizing Redux is an effortless affair because it supports middleware.
  • Reusability. Redux is framework-agnostic, so it is very good at reusability.
  • Ecosystem. Redux offers a giant ecosystem of helpful add-ons, libraries, and tools.
  • Community. Redux, the oldest state management library in our comparison, has amassed a large community with a significant knowledge base. There are ~30,000 (~19,000 answered) questions with a redux tag on Stack Overflow.
  • Pulse. Redux is updated and maintained regularly.

MobX

MobX is another relatively old library with 23,000 stars on GitHub. What sets it apart from Redux is that it follows the OOP paradigm and uses observables. MobX was created by Michel Weststrate and it’s currently maintained by a group of open-source enthusiasts with the help of Boston-based Mendix.

Diagram depicting state management using MobX, from actions, through observable states and computed values, to side effects.

In MobX, you create a JavaScript class with a makeObservable call inside the constructor that is your observable store (you can use @observable decorator if you have the appropriate loader). Then you declare properties (state) and methods (actions and computed values) of the class. The components subscribe to this observable store to access the state, calculated values, and actions.

Another essential feature of MobX is mutability. It allows updating the state silently in case you want to avoid side effects.

MobX API Example

A unique feature of MobX is that you create almost pure ES6 classes with all the magic hidden under the hood. It requires less library-specific code to keep the concentration on the logic.

CodeSandbox

// stores/counter.js
import { makeAutoObservable } from "mobx";

class CounterStore {
  value = 0;

  constructor() {
    makeAutoObservable(this);
  }

  increment() {
    this.value += 1;
  }

  decrement() {
    this.value -= 1;
  }
}

export default CounterStore;


// index.js
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "mobx-react";
import App from "./App";
import CounterStore from "./stores/counter";

ReactDOM.render(
  <Provider counter={new CounterStore()}>
    <App />
  </Provider>,
  document.getElementById("root")
);


// App.js
import React from "react";
import { inject, observer } from "mobx-react";

const App = inject((stores) => ({ counter: stores.counter }))(
  observer(({ counter }) => {
    return (
      <div>
        <div>
          <button onClick={() => counter.increment()}>Increment</button>
          <span>{counter.value}</span>
          <button onClick={() => counter.decrement()}>Decrement</button>
        </div>
      </div>
    );
  })
);

export default App;

Quality Attributes

  • Usability. An observable store is the single entry point for state management. It makes usage of MobX simple because you have the only place to modify.
  • Maintainability. It’s a considerable downside. Without knowledge of RxJS API, you won’t be able to achieve the desired result. Using MobX in a poorly qualified team may lead to state inconsistency troubles.
  • Performance. MobX consists of independent stores and enables you to subscribe to the only ones you need. It’s very effective.
  • Testability. Observable stores are plain JavaScript objects with reactive functionality hidden inside. Testing is the same as for any other JavaScript class.
  • Scalability. Observable stores are split logically; there is no difficulty in scaling MobX.
  • Modifiability. MobX allows creating custom observables with modified behaviors. In addition, there is a concept called reactions. Reactions model automatic side effects. These things make MobX very customizable.
  • Reusability. MobX is agnostic of frameworks, so it is very good at reusability.
  • Ecosystem. There are hundreds of extensions available for MobX.
  • Community. MobX has a lot of devoted fans. There are ~1,600 (~1,000 answered) questions with the mobx tag on Stack Overflow.
  • Pulse. MobX is updated and maintained regularly.

Recoil

Recoil is a relative newcomer, the latest brainchild of the React team. The basic idea behind it is a simple implementation of missing React features like shared state and derived data.

You might be wondering why an experimental library is reviewed for enterprise-level projects. First of all, Recoil is one of the most discussed topics in the React community at the moment. Secondly, Recoil is backed by Facebook and already used in some of its applications, meaning that it will become a stable version at some point. Finally, it’s an entirely new approach for sharing state in React, and I’m sure that even if Recoil is deprecated, there will be another tool that follows the same path.

Recoil is built on top of two terms: atom and selector. An atom is a shared-state piece. A component can subscribe to an atom to get/set its value.

Diagram depicting state management with Recoil, showing how components can subscribe to an atom to retrieve or set its value.

As you can see in the image, only the subscribed components are re-rendered when the value is changed. It makes Recoil very performant.

Another great thing Recoil has out of the box is the selector. The selector is a value aggregated from an atom or other selector. For consumers, there is no difference between atom and selector, they just need to subscribe to some reactive part and use it.

Diagram illustrating the use of selectors in Recoil, their relation to atoms, and changes caused by different values.

Whenever an atom/selector is changed, the selectors that use it (i.e., are subscribed to it) are reevaluated.

Recoil API Example

Recoil’s code is far more different than its competitors. It’s based on React hooks and focuses more on the state structure than on mutating this state.

CodeSandbox

// atoms/counter.js
import { atom } from "recoil";

const counterAtom = atom({
  key: "counter",
  default: 0
});

export default counterAtom;


// index.js
import React from "react";
import ReactDOM from "react-dom";
import { RecoilRoot } from "recoil";
import App from "./App";

ReactDOM.render(
  <RecoilRoot>
    <App />
  </RecoilRoot>,
  document.getElementById("root")
);


// App.js
import React from "react";
import { useRecoilState } from "recoil";
import counterAtom from "./atoms/counter";

const App = () => {
  const [count, setCount] = useRecoilState(counterAtom);

  return (
    <div>
      <div>
        <button onClick={() => setCount(count + 1)}>Increment</button>
        <span>{count}</span>
        <button onClick={() => setCount(count - 1)}>Decrement</button>
      </div>
    </div>
  );
};

export default App;

Quality Attributes

  • Usability. Recoil is one of the easiest tools to use because it works like useState in React.
  • Maintainability. All you have to do in Recoil is maintain selectors and hooks inside the components—more value, less boilerplate.
  • Performance. Recoil builds a state tree outside of React. The state tree enables you to get and listen to the things you need, not the whole tree’s changes. It’s also well-optimized under the hood.
  • Testability. Recoil provides a mechanism for testing its atoms and selectors.
  • Scalability. A state that splits into multiple independent pieces makes it a good player in scalability.
  • Modifiability. Recoil is only responsible for storing values and their aggregations. It has no data flow so it can be customized easily.
  • Reusability. Recoil relies on React. It can’t be reused elsewhere.
  • Ecosystem. There is no ecosystem for Recoil at the moment.
  • Community. Recoil is too fresh to have a big community. There are ~70 questions with recoiljs tag on Stack Overflow.
  • Pulse. Recoil is updated infrequently (six months elapsed between its two most recent updates). It also has plenty of open issues on GitHub.

Choosing the Right React State Management Tool

How do these React global state management libraries stack up when it comes to enterprise-grade apps?

Recoil is young and fresh, but has no community nor ecosystem at the moment. Even though Facebook is working on it and the API seems promising, a huge React application cannot rely on a library with weak community support. In addition, it’s experimental, making it even more unsafe. It’s definitely not a good option for React enterprise applications today but it’s worth keeping an eye on it.

MobX and Redux do not share any of these issues and most big players on the market use them. What makes them different from each other is the learning curve. MobX requires a basic understanding of reactive programming. If the engineers involved in the project are not skilled enough, the application may end up with code inconsistencies, performance issues, and increased development time. MobX is acceptable and will meet your needs if your team is aware of reactivity.

Redux has some issues as well, mostly regarding scalability and performance. However, unlike MobX, there are proven solutions to these problems.

Taking every advantage and disadvantage into account, and considering my personal experience, I recommend Redux as the best option for React enterprise-level applications.

Understanding the basics

React is a library that generates a visual interface. The visual representation directly depends on the state. In other words, the state is responsible for what the user sees. Any change in your state is immediately shown to the user.

React’s useState is the best option for local state management. If you need a global state solution, the most popular ones are Redux, MobX, and built-in Context API. Your choice will depend on the size of your project, your needs, and your engineers’ expertise.

Maintaining global state is easy if you keep it clean. It should contain only those items that are shared across multiple loosely connected components.

Context API’s functionality is tiny out of the box. It was not built and optimized for high-frequency updates but rather for low-frequency updates like theme updates and authentication management.

With the introduction of the official Redux Toolkit, describing state management in Redux has become concise.

MobX is global state management implemented with RxJS. If you want to become an ace, learn RxJS. It’ll be helpful not only for understanding a single library but for the whole concept of reactivity.

Recoil performs well, even in its experimental stage. It's not ideal for a large application, but it would be useful for a tiny one that does not depend on the state too much.