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!
Top comments (53)
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.
It looks like you and I had the same DX with Redux 😆
+1
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.
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.
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.
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
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?
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.
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:
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
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.
Do whatever you believe just keep in mind that context is not a state manager.
I would say that a "moderately sized" app is one which does not have:
When you start to have complications like:
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.
Atom-based Libraries
Atom-based Libraries / Weaknesses
Jotai
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-...
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 :(
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.
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
oruseMemo
or evenmemo
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:
As you can see, in this example,
OtherComponent
would be re-rendered even if valuesa
orc
were not modified.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!
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 🤗
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.
Good. Always used proxy-based MobX singletons with Typescript. Best combo
MobX is awesome, I recently use legend state as an alternative. It has everything mobx provide but has less boilerplate
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.
Learned something new, thanks for sharing!
zustand
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.