React State Management in 2024
Hồng Phát
Posted on December 8, 2023
In my POV, React state management libraries can be divided into three groups:
- Reducer-based: requires dispatching actions to update a big centralised state, often called a “single source of truth”. In this group, we have Redux and Zustand.
- Atom-based: splits states into tiny pieces of data called atoms, which can be written to and read from using React hooks. In this group, we have Recoil and Jotai.
- Mutable-based: leverages proxy to create mutable data sources which can be directly written to or reactively read from. Candidates in this group are MobX and Valtio.
Now that we've covered the three main categories of React state management libraries. Let's delve deeper into each one and explore the strengths and weaknesses of each approach. This will help you understand which library best suits your project's needs:
1. Reducer-based Libraries:
Despite its common criticism about being (overly) complicated, Redux has been the most popular state management library since its creation.
+---------------------+
| Actions |
+----------|----------+
|
v
+---------------------+ +---------------------+
| Reducers | | Store |
+----------|----------+ +----------|----------+
| |
v v
+---------------------+ +---------------------+
| State | | Subscriptions |
+---------------------+ +---------------------+
Strengths:
A powerful state machine and time machine. Suppose all of your application states live inside the centralised state (which rarely happens because you might have local states in your components), this formula will exist:
UI = React(state)
. This means a single state value will only result in one UI, so your application will look consistently the same with a specific state. If you backup the entire state somewhere, then dispatch a change likeREVERT(pastState) { state = pastState }
, your UI will be restored as if it was a captured screenshot.The best DevTools support: By updating the state using explicit actions, DevTools can help you point out what, when and how the state changes. You can imagine it like having a Git commit history in your application state, how cool is it?
Weaknesses:
- Boilerplate code: even a simple change to your state requires considerable changes in the code.
- Steep learning curve: while it is simple at its core, it is never enough on its own. To truly master Redux, you should know how to use it with other libraries such as Saga, Thunk, Reselect, Immer, or Redux Toolkit. It feels overkill when most of the time, we use generators in Saga just to fetch some data over the network. Modern JS developers tend to use async/await on a day-by-day basis.
- TypeScript: although fully support TypeScript, explicit typing is required most of the time to get typing done for actions, reducers, selectors, and state. Other approaches directly support automatic type inference.
2. Atom-based Libraries:
Instead of putting your whole application state inside a large centralised state, this approach splits it into multiple atoms, each atom preferably as tiny as primitive types or basic data structures like arrays and flat objects. Then, you can use the selector to group related states together later if you need to.
+---------------------+
| Atoms (State) |
+----------|----------+
|
v
+---------------------+ +---------------------+
| Selectors (Derived | | RecoilRoot |
| State) | +----------|----------+
+----------|----------+ |
v v
+---------------------+ +---------------------+
| State Snapshot | | React Components |
+---------------------+ +---------------------+
Strengths:
Leverage React features: this is expected since Recoil and React are both created by Facebook. Recoil works great with cutting-edge React features such as Suspense, Transition API and Hooks.
Simple and scalable: by using only atoms and selectors, you can still effectively build up a giant reactive application state while having fine-grained control over individual state changes. Lifting state up is now as simple as declaring an atom and changing your
useState
hook touseRecoilState
.TypeScript: as a developer who cares about DX as much as a user cares about UI and UX, I found React, Recoil, and TypeScript to be a wonderful combination. In my projects, types are automatically inferred most of the time.
Weaknesses:
DevTools: if you are looking for an equivalent of Redux DevTools, unfortunately, there isn’t.
Cannot use state outside of components: although Recoil Nexus is a workaround, this kind of state management library is designed with a (maybe true) assumption that all usage of state happens inside React components.
Not stable (yet): it has been 4 years, and the latest version of Recoil still has the leading 0 (v0.7.7). I would be glad if, by the time you read this, this information stays irrelevant.
3. Mutable-based Libraries:
Tips: "mutable" and "immutable" refer to how data can be changed after it is created:
person.age += 1 // mutable
person = { …person, age: person.age + 1 } // immutable
+---------------------+
| Observables |
+----------|----------+
|
v
+---------------------+ +---------------------+
| Computed Values | | Actions |
+----------|----------+ +----------|----------+
| |
v v
+---------------------+ +---------------------+
| Reaction (Derived | | MobX Store |
| Value) | +----------|----------+
+---------------------+ |
v
+---------------------+
| React Components |
+---------------------+
Strengths:
- The simplest API: by allowing the state to be mutated directly, no boilerplate code is required to sit between your component and state, unless you want to do so.
- Reactivity and flexibility: dependencies are updated automatically whenever the state changes. This simplifies your application logic and makes it easier to comprehend. Moreover, the proxy-based approach helps minimise unnecessary re-renders. This also translates to smooth performance and a more responsive user experience.
Weaknesses:
- Too much magic: automatic reactivity is a double-edged sword. Race conditions in asynchronous updates can lead your application state to chaos, and debugging the flow of changes can be challenging in complex applications.
- DevTools: again, it seems to me that no alternative has the best tooling support as the reducer-based approach.
- Discrete DX: while React elaborates on the “immutable” approach, having “mutable” data mixed in my project sometimes makes me feel insecure about how I should make changes to my data.
The best choice
Again, the best React state management library for your project depends on your and your team’s specific needs and expertise. Please DON'T:
Pick a library based solely on project size and complexity. Because, you may have heard somewhere that X is more suitable for a large-scale project while Y is better for a smaller one. Library authors designed their libraries with scalability in mind, and your project’s scalability depends on how you write the code and use the library, not which libraries you choose to work with.
Apply best practices you learned from one library to another. Putting your whole application state inside a single Recoil atom to achieve a “single source of truth” will only lead to struggling with state updates and issues with performance. As well as defining actions in Redux as if they were setters and dispatching multiple of them instead of batching changes in one commit.
The author's choice
TL;DR: Jotai.
I personally prefer the atomic libraries because of the advantages listed above and my historical painless DX when dealing with asynchronous data fetching and batching loading UI with <Suspense>
. What Jotai does better than Recoil is that:
- No key is required. Naming things is tough, and most of the time, you won’t use Recoil’s keys. So why spend time declaring them at all when the libraries can automatically have the keys for you? Here is Recoil’s answer; however, as you can see, people are not quite convinced.
- Performance. A picture is worth a thousand words, and I have 4 of them:
You might argue that a ~20Kb difference in size does not matter that much, but let’s take a look at a benchmark which was taken on a very old Android device, where sluggishness appears as obvious as bars filled in with a pattern of diagonal red stripes. As you can see, Jotai internal logic requires less overall calculation, which improved my application's LCP, an important Core Web Vitals metric, from ~2.6s to ~1.2s. Nonetheless, this comparison may not take into account other factors that Recoil do better than Jotai (in fact, my knowledge cutoff in this). I just want to say that the Jotai team did a wonderful job there.
I hope this helps!
Posted on December 8, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.