DEV Community

Cover image for React State Management in 2024
Hồng Phát
Hồng Phát

Posted on • Edited on

React State Management in 2024

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   |
+---------------------+        +---------------------+


Enter fullscreen mode Exit fullscreen mode

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 like REVERT(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?
    Commit messages meme. Source: https://medium.com/@hritik.jaiswal/how-to-write-a-good-commit-message-9d2d533b9052

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  |
+---------------------+        +---------------------+


Enter fullscreen mode Exit fullscreen mode

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 to useRecoilState.

  • 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  |
                               +---------------------+


Enter fullscreen mode Exit fullscreen mode

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:

Library Import cost LCP
Recoil Recoil import cost Recoil usage's LCP
Jotai Jotai import cost Jotai usage's LCP

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!

Top comments (53)

Collapse
 
ghamadi profile image
Ghaleb • Edited

The only good thing Redux still has going for it is its Devtools, and that will never be a good enough reason for me to use it. It's the most used tool because it's the earliest robust tool and goes back to the days of class components.

Redux, and central state management in general, make needless globalization the go-to approach to share state, which can quickly turn into a nightmare.

I am convinced that the notion that "central stores are suitable for large projects" is the biggest sham in history. I know from experience that the opposite is more likely to be true.

Central stores are only good for bigger apps only when most state is global by definition. That's it. And I don't know many such use-cases.

Collapse
 
nguyenhongphat0 profile image
Hồng Phát

It looks like you and I had the same DX with Redux 😆

Collapse
 
boas19910115 profile image
Gongben

+1

Collapse
 
ichchkv profile image
Ivan Chichkov • Edited

If react offers a context and providers and a way to consume the context values using hooks, you must have really specific needs to go and search for libraries. I believe 90% of projects don't.
So, use your common senses, as first step, then plan further.

Collapse
 
ivan_jrmc profile image
Ivan Jeremic

The need is most often the re-rendering the whole app problem. It is better to re-render a few components than the whole tree, that's also the reason why you should work with multiple contexts and not just with one which however becomes hard to manage.

Collapse
 
ichchkv profile image
Ivan Chichkov

Yes, but you can have as many context providers as you need, as atomic as you need them to be. Provide them as hooks and use them. You'll avoid the dependency to any 3rd party libs and go with pure react. You'll have full control over what you provide and the scope you consume your context in.
Never understood overcomplicating our own work with additional libs if we can achieve our goals with the tools at hand.
There should be real need to solve a problem when you make a decision and first thing is to try to write some code instead jumping on libs wagon ... Libs are good think but they are created to solve problems where they exist.

Thread Thread
 
silverium profile image
Soldeplata Saketos

but the moment your app needs a global context provider (let's say for global notifications), then the app is screwed with multiple rerenders with Context

Thread Thread
 
ichchkv profile image
Ivan Chichkov

It is getting interesting .. and why you think so?
It is up to you when you re-render and what, no? And why any change in whatever of your context hooks will cause re-render of anything you do not change?

Thread Thread
 
ivan_jrmc profile image
Ivan Jeremic

It is not as easy as you might think, there is a reason why the number one rule to learn is that Context alone is not a state management solution.

Thread Thread
 
silverium profile image
Soldeplata Saketos

Well, I think there is a misunderstanding of what is a re-render and what is a re-computation.

A re-computation is when the current component is called again (as a function) and all logic inside is being triggered.
Example:

import * as React from "react";

let externalCounter = 1;
const LoggingComponent = () => {
  const [internalCounter, setInternalCounter] = React.useState(1);
  const [, forceRender] = React.useState(1);
  externalCounter++;
  console.log(
    `current internalCounter is ${internalCounter} and external is ${externalCounter}`
  );
  return (
    <>
      <button onClick={() => setInternalCounter((p) => p + 1)}>
        {internalCounter}
      </button>
      <button onClick={() => forceRender((p) => p + 1)}>Force recompute</button>
    </>
  );
};
export default LoggingComponent;
Enter fullscreen mode Exit fullscreen mode

You will see that the LogginComponent is re-computed and the console log will show that the externalCounter is increasing every time you click the "Force recompute" button. Something similar happens with the useReducer

Thread Thread
 
ichchkv profile image
Ivan Chichkov

Hmm, shall you give a thought about what it will be for moderately sized and complex application to get dependent on some libraries that are perfect, but not actually needed?
In summary, React's Context API, when used judiciously with React Hooks, provides a more efficient way to manage state and control rendering behavior in a React application. It offers a simpler and often sufficient alternative to more complex state management libraries for many use cases, especially in moderately sized applications, as I mentioned above.
So, instead of 'first rules' we better use our common senses' as rules often are subjective to the use case or the user.

Thread Thread
 
ivan_jrmc profile image
Ivan Jeremic

Do whatever you believe just keep in mind that context is not a state manager.

Thread Thread
 
silverium profile image
Soldeplata Saketos

I would say that a "moderately sized" app is one which does not have:

  • full page overlays, pages with entities (models, data, whatever you call it) that need to be kept in memory while client side navigating to other pages
  • actions that trigger changes that affect multiple entities
  • more than 20 pages

When you start to have complications like:

  • user state management
  • shopping carts
  • messaging
  • global banners, popups, etc
  • dynamic theming, translations, and global complex stuff Then it's better to rely in Redux, with RTK, which will provide a super nice DX with all actions centralized, visible state tree, independent slices, re-select, etc
Collapse
 
ivan_jrmc profile image
Ivan Jeremic

I know how context works, and I also wrote context like you do since 2019, you are misunderstanding a few thing and you think you simplifying while in reality you write more code. I bet if you show me a project where you do this I will find tons of unessecary re-renders.

Collapse
 
adaptive-shield-matrix profile image
Adaptive Shield Matrix • Edited

Atom-based Libraries

  • Are the best approach, because you do not have to have a global state and can select and just use different state parts (atoms) that you need.
  • It blows any other state management solution out of the way by provided developer experience and ease of use.

Atom-based Libraries / Weaknesses

  • I would not list DevTools as a downside, since no global state management solution (including react context hooks) allows you to
  • all listed waeknesses are about recoil
  • Jotai has no weaknesses

Jotai

  • ask anyone, who tried different/all state managers, and everyone will tell you that Jotai is the best
  • DevTools: jotai provides an api to see/log state updates
  • has best and most helpful community/support on github

Generel

20Kb difference is huge, who says it is not?
Only one who can that are people using Nextjs that ships 200kb of JS even in a simple hello world example -> RIP all old/cheap smartphone mobile users (and all users of your app/website in third world countries).

I have written an article, where I touched on state management in react
dev.to/adaptive-shield-matrix/reac...
TLDR: no one should use react build-in state managers. Pick one library - Jotai (recommended) or Zustand and use it exclusively.

Here is some prior art comparing react state managers, about a year ago
frontendmastery.com/posts/the-new-...

Collapse
 
brense profile image
Rense Bakker

I'm not even sure why recoil still exists tbh... They were really good at SEO for a long time, thats for sure, but that discouraged a lot of people from using atoms, because they would stumble upon recoil first, before jotai, when they searched for atom state management. Luckily that's no longer the case, but we can see that a lot of people still asociate atoms with recoil :(

Collapse
 
pengeszikra profile image
Peter Vivo

My personal solution of state management is useReducer which is a ligthweight replacer of redux. A small 100LOC code npm module written in typescript.
(react-state-factory)npmjs.com/package/react-state-factory[dev.to/pengeszikra/simplify-your-r...]
I give a typescript helper for state handling user can define own - even complex state type - and pass to useReducer.
This type of state is not mandatory use at global statate level, and boiler plate near easy as useState.

Collapse
 
silverium profile image
Soldeplata Saketos

The only problem with this approach (if any) is that the state is updated in every single action, triggering a re-computation of the component, which might lead to re-assignments of any other logic inside the component, and could potentially lead to re-renders if the devs are not using useCallback or useMemo or even memo depending on the case.

So, for this to properly work, you need the devs in your team to be aware of the pitfalls to be avoided.
Example of bad implementation:

const Parent = () =>{
   const [state, dispatch] = useReducer(someReducer, null, { a: 1, b:1, c:1});

  const someComputedValue = a  / c;

  return <div>
                <OtherComponent value={someComputedValue} />
                <button onClick={()=> dispatch({ type: "set_b", payload: 42})}> set b to 42</button>
           </div>
}
Enter fullscreen mode Exit fullscreen mode

As you can see, in this example, OtherComponent would be re-rendered even if values a or c were not modified.

Collapse
 
karanborana1210 profile image
karanborana1210 • Edited

Great article @nguyenhongphat0 . I recently found Jotai, and I am blown away by it's simplicity and DX. But, I am still not sure what will be the best practices for using Jotai in a big project where there is big state object to manage. I haven't got any example of such. If you have any idea on this, please write another article on this, will be of great help 🙌. Thanks!

Collapse
 
nguyenhongphat0 profile image
Hồng Phát

Thank you for your compliment, I really appreciate it! I would love to share my specific experience with Jotai and tips that I found useful in another blog. Stay tuned 🤗

Collapse
 
karanborana1210 profile image
karanborana1210

Sure @nguyenhongphat0 . I have been using Jotai for a while now, but only for small projects as of now. Haven't been able to decode the best practice to use it in large projects. Will be waiting for your article.

Collapse
 
fosteman profile image
Timothy Fosteman

Good. Always used proxy-based MobX singletons with Typescript. Best combo

Collapse
 
daliskafroyan profile image
Royan Daliska

MobX is awesome, I recently use legend state as an alternative. It has everything mobx provide but has less boilerplate

Collapse
 
merri profile image
Vesa Piittinen

Signals.

Although React is so 2018 that you might want to consider something else. Like, something that actually has been designed to work great with signals.

Collapse
 
christian_go3 profile image
Christian GO

Learned something new, thanks for sharing!

Collapse
 
funnypan profile image
panfan

zustand

Collapse
 
fantasticsoul profile image
幻魂

try helux github.com/heluxjs/helux
About
A state engine integrates atom, signal, derive, watch and dep tracking, supports fine-grained responsive updates, it is compatible with all react like libs (including React 18)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.