State management in React has been a hotly debated topic for years, yet little attention seems to be paid to enterprise-level applications and their specific requirements. Let’s take a closer look and compare three of the most popular state management tools available today.
authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Editor’s note: This article was updated on October 17, 2022, by our editorial team. It has been modified to use embedded code demos, reference more recent data, and align with our current editorial standards.
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 analyze and compare the most popular libraries and pick the most appropriate one for React state management in an enterprise-level application.
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.
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.
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.
As React team member Sebastian Markbage mentioned, the Context API was not built and optimized for high-frequency updates but rather for low-frequency updateslike theme updates and authentication management.
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 used the following quality attributes:
Redux is a state container created in 2015. It became wildly popular because:
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 need to subscribe the components to the store updates via react-redux.
Slices are the fundamental part of the Redux codebase that differentiates it from the other tools. Slices contain all the logic of actions and reducers.
// 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;
MobX is another relatively old library with ~25,800 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.
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.
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.
// 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;
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. Recoil is backed by Facebook and used in some of its applications, and has brought an entirely new approach to sharing state in React. I’m sure that even if Recoil is deprecated, another tool that follows the same path, like Jotai, will gain traction.
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.
As you see in the image, only the subscribed components are rerendered when the value changes. 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.
Whenever an atom/selector is changed, the selectors that use it (i.e., are subscribed to it) are reevaluated.
Recoil’s code is considerably different from its competitors. It’s based on React hooks and focuses more on the state structure than on mutating the state.
// 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;
These React global state management libraries offer different pros and cons 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 have any of these issues, and most big players on the market use them. What makes them different from each other are their respective learning curves. MobX requires a basic understanding of reactive programming. If the engineers involved in a 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.
This blog post was originally published at: Source