Skip to main content

· 15 min read

cover photo

Photo by Mark Basarab on Unsplash

(Originally posted on HackerNoon).

Recoil introduced the atomic model to the React world. Its new powers came at the cost of a steep learning curve and sparse learning resources.

Jotai and Zedux later simplified various aspects of this new model, offering many new features and pushing the limits of this amazing new paradigm.

Other articles will focus on the differences between these tools. This article will focus on one big feature that all 3 have in common:

They fixed Flux.

Flux

If you don't know Flux, here's a quick gist:

Actions -> Dispatcher -> Stores -> Views

Besides Redux, all Flux-based libraries basically followed this pattern: An app has multiple stores. There is only one Dispatcher whose job is to feed actions to all the stores in a proper order. This "proper order" means dynamically sorting out dependencies between the stores.

For example, take an e-commerce app setup:

UserStore <-> CartStore <-> PromosStore

When the user moves, say, a banana to their cart, the PromosStore needs to wait for CartStore's state to update before sending off a request to see if there's an available banana coupon.

Or perhaps bananas can't ship to the user's area. The CartStore needs to check the UserStore before updating. Or perhaps coupons can only be used once a week. The PromosStore needs to check the UserStore before sending the coupon request.

Flux doesn't like these dependencies. From the legacy React docs:

The objects within a Flux application are highly decoupled, and adhere very strongly to the Law of Demeter, the principle that each object within a system should know as little as possible about the other objects in the system.

The theory behind this is solid. 100%. Soo ... why did this multi-store flavor of Flux die?

Dependency Trees

Well turns out, dependencies between isolated state containers are inevitable. In fact, to keep code modular and DRY, you should be using other stores frequently.

In Flux, these dependencies are created on the fly:

// This example uses Facebook's own `flux` library
PromosStore.dispatchToken = dispatcher.register(payload => {
if (payload.actionType === 'add-to-cart') {
// wait for CartStore to update first:
dispatcher.waitFor([CartStore.dispatchToken])

// now send the request
sendPromosRequest(UserStore.userId, CartStore.items).then(promos => {
dispatcher.dispatch({ actionType: 'promos-fetched', promos })
})
}

if (payload.actionType === 'promos-fetched') {
PromosStore.setPromos(payload.promos)
}
})

CartStore.dispatchToken = dispatcher.register(payload => {
if (payload.actionType === 'add-to-cart') {
// wait for UserStore to update first:
dispatcher.waitFor([UserStore.dispatchToken])

if (UserStore.canBuy(payload.item)) {
CartStore.addItem(payload.item)
}
}
})

This example shows how dependencies aren't directly declared between stores - rather, they're pieced together on a per-action basis. These informal dependencies require digging through implementation code to find.

This is a very simple example! But you can already see how helter-skelter Flux feels. Side effects, selection operations, and state updates are all cobbled together. This colocation can actually be kind of nice. But mix in some informal dependencies, triple the recipe, and serve it on some boilerplate and you'll see Flux break down quickly.

Other Flux implementations like Flummox and Reflux improved the boilerplate and debugging experience. While very usable, dependency management was the one nagging problem that plagued all Flux implementations. Using another store felt ugly. Deeply-nested dependency trees were hard to follow.

Flux in Theory vs Flux in Practice

This ecommerce app could someday have stores for OrderHistory, ShippingCalculator, DeliveryEstimate, BananasHoarded, etc. A large app could easily have hundreds of stores. How do you keep dependencies up-to-date in every store? How do you track side effects? What about purity? What about debugging? Are bananas really a berry?

As for the programming principles introduced by Flux, unidirectional data flow was a winner, but, for now, the Law of Demeter was not.

The Singleton Model

We all know how Redux came roaring in to save the day. It ditched the concept of multiple stores in favor of a singleton model. Now everything can access everything else without any "dependencies" at all.

Actions -> Middleware -> Store -> Views

Reducers are pure, so all logic dealing with multiple state slices must go outside the store. The community made standards for managing side effects and derived state. Redux stores are beautifully debuggable. The only major Flux Flaw that Redux originally failed to fix was its boilerplate.

RTK later simplified Redux's infamous boilerplate. Then Zustand removed some fluff at the cost of some debugging power. All of these tools have become extremely popular in the React world.

With modular state, dependency trees grow so naturally complex that the best solution we could think of was, "Just don't do it I guess."

Got problems? Just don't

And it worked! This new singleton approach still works well enough for most apps. The Flux principles were so solid that simply removing the dependency nightmare fixed it.

Or did it?

Back to Basics

The success of the singleton approach begs the question, what was Flux getting at in the first place? Why did we ever want multiple stores?

Allow me to shed some light on this.

Reason #1: Autonomy

With multiple stores, pieces of state are broken out into their own autonomous, modular containers. These stores can be tested in isolation. They can also be shared easily between apps and packages.

Reason #2: Code Splitting

These autonomous stores can be split into separate code chunks. In a browser, they can be lazy-loaded and plugged in on the fly.

Redux's reducers are fairly easy to code-split too. Thanks to replaceReducer, the only extra step is to create the new combined reducer. However, more steps may be required when side effects and middleware are involved.

Reason #3: Standardized Primitives

With the singleton model, it's difficult to know how to integrate an external module's internal state with your own. The Redux community introduced the Ducks pattern as an attempt to solve this. And it works, at the cost of a little boilerplate.

With multiple stores, an external module can simply expose a store. For example, a form library can export a FormStore. The advantage of this is that the standard is "official", meaning people are less likely to create their own methodologies. This leads to a more robust, unified community and package ecosystem.

Reason #4: Scalability

The singleton model is surprisingly performant. Redux has proven that. However, its selection model especially has a hard upper limit. I wrote some thoughts on this in this Reselect discussion. A big, expensive selector tree can really start to drag, even when taking maximum control over caching.

On the other hand, with multiple stores, most state updates are isolated to a small portion of the state tree. They don't touch anything else in the system. This is scalable far beyond the singleton approach - in fact, with multiple stores, it's very difficult to hit CPU limitations before hitting memory limitations on the user's machine.

Reason #5: Destruction

Destroying state is not too difficult in Redux. Just like in the code-splitting example, it requires only a few extra steps to remove a part of the reducer hierarchy. But it's still simpler with multiple stores - in theory, you can simply detach the store from the dispatcher and allow it to be garbage collected.

Reason #6: Colocation

This is the big one that Redux, Zustand, and the singleton model in general do not handle well. Side effects are separated from the state they interact with. Selection logic is separated from everything. While multi-store Flux was perhaps too colocated, Redux went to the opposite extreme.

With multiple autonomous stores, these things naturally go together. Really, Flux only lacked a few standards to prevent everything from becoming a helter-skelter hodge-podge of gobbledygook (sorry).

Reasons Summary

Now, if you know the OG Flux library, you know that it actually wasn't great at all of these. The dispatcher still takes a global approach - dispatching every action to every store. The whole thing with informal/implicit dependencies also made code splitting and destruction less than perfect.

Still, Flux had a lot of cool features going for it. Plus the multiple-store approach has potential for even more features like Inversion of Control and fractal (aka local) state management.

Flux might have evolved into a truly powerful state manager if somebody hadn't named their goddess Demeter. I'm serious! ... Ok, I'm not. But now that you mention it, maybe Demeter's law deserves a closer look:

The Law of Demeter

What exactly is this so-called "law"? From Wikipedia:

  • Each unit should have only limited knowledge about other units: only units "closely" related to the current unit.

  • Each unit should only talk to its friends; don't talk to strangers.

This law was designed with Object-Oriented Programming in mind, but it can be applied in many areas, including React state management.

PromosStore shouldn't use CartStore's internal state or dependencies

The basic idea is to prevent a store from:

  • Tightly-coupling itself to another store's implementation details.
  • Using stores that it doesn't need to know about.
  • Using any other store without explicitly declaring a dependency on that store.

In banana terms, a banana shouldn't peel another banana and shouldn't talk to a banana in another tree. However, it can talk to the other tree if the two trees rig up a banana phone line first.

This encourages separation of concerns and helps your code stay modular, DRY, and SOLID. Solid theory! So what was Flux missing?

Well, inter-store dependencies are a natural part of a good, modular system. If a store needs to add another dependency, it should do that and do it as explicitly as possible. Here's some of that Flux code again:

PromosStore.dispatchToken = dispatcher.register(payload => {
if (payload.actionType === 'add-to-cart') {
// wait for CartStore to update first:
dispatcher.waitFor([CartStore.dispatchToken])

// now send the request
sendPromosRequest(UserStore.userId, CartStore.items).then(promos => {
dispatcher.dispatch({ actionType: 'promos-fetched', promos })
})
}

if (payload.actionType === 'promos-fetched') {
PromosStore.setPromos(payload.promos)
}
})

PromosStore has multiple dependencies declared in different ways - it waits for and reads from CartStore and it reads from UserStore. The only way to discover these dependencies is to look for stores in PromosStore's implementation.

Dev tools can't help make these dependencies more discoverable either. In other words, the dependencies are too implicit.

While this is a very simple and contrived example, it illustrates how Flux misinterpreted the Law of Demeter. While I'm sure it was mostly born of a desire to keep Flux implementations small (real dependency management is a complex task!), this is where Flux fell short.

Unlike the heroes of this story:

The Heroes

In 2020, Recoil came stumbling onto the scene. While a little clumsy at first, it taught us a new pattern that revived the multiple-store approach of Flux.

Unidirectional data flow moved from the store itself to the dependency graph. Stores were now called atoms. Atoms were properly autonomous and code-splittable. They had new powers like suspense support and hydration. And most importantly, atoms formally declare their dependencies.

The atomic model was born.

// a Recoil atom
const greetingAtom = atom({
key: 'greeting',
default: 'Hello, World!',
})

Recoil struggled with a bloated codebase, memory leaks, bad performance, slow development, and unstable features - most notably side effects. It would slowly iron out some of these, but in the meantime, other libraries took Recoil's ideas and ran with them.

Jotai burst onto the scene and quickly gained a following.

// a Jotai atom
const greetingAtom = atom('Hello, World!')

Besides being a tiny fraction of Recoil's size, Jotai offered better performance, sleeker APIs, and no memory leaks due to its WeakMap-based approach.

However, it came at the cost of some power - the WeakMap approach makes cache control difficult and sharing state between multiple windows or other realms almost impossible. And the lack of string keys, while sleek, makes debugging a nightmare. Most apps should add those back in, drastically tarnishing Jotai's sleekness.

// a (better?) Jotai atom
const greetingAtom = atom('Hello, World!')
greetingAtom.debugLabel = 'greeting'

A few honorable mentions are Reatom and Nanostores. These libraries have explored more of the theory behind the atomic model and try to push its size and speed to the limit.

The atomic model is fast and scales very well. But up until very recently, there were a few concerns that no atomic library had addressed very well:

  • The learning curve. Atoms are different. How do we make these concepts approachable for React devs?
  • Dev X and debugging. How do we make atoms discoverable? How do you track updates or enforce good practices?
  • Incremental migration for existing codebases. How do you access external stores? How do you keep existing logic intact? How do you avoid a full rewrite?
  • Plugins. How do we make the atomic model extensible? Can it handle every possible situation?
  • Dependency Injection. Atoms naturally define dependencies, but can they be swapped out during testing or in different environments?
  • The Law of Demeter. How do we hide implementation details and prevent scattered updates?

This is where I come in. See, I'm the principle creator of another atomic library:

Zedux

Zedux finally entered the scene a few weeks ago. Developed by a Fintech company in New York - the company I work for - Zedux was not only designed to be fast and scalable, but also to provide a sleek development and debugging experience.

// a Zedux atom
const greetingAtom = atom('greeting', 'Hello, World!')

I won't go into depth about Zedux's features here - as I said, this article won't focus on the differences between these atomic libraries.

Suffice it to say that Zedux addresses all the above concerns. For example, it's the first atomic library to offer real Inversion of Control and the first to bring us full circle back to the Law of Demeter by offering atom exports for hiding implementation details.

The last ideologies of Flux have finally been revived - not only revived but improved! - thanks to the atomic model.

So what exactly is the atomic model?

The Atomic Model

These atomic libraries have many differences - they even have different definitions of what "atomic" means. The general consensus is that atoms are small, isolated, autonomous state containers reactively updated via a Directed Acyclic Graph.

I know, I know, it sounds complex, but just wait till I explain it with bananas.

I'm kidding! It's actually really simple:

Update -> UserAtom -> CartAtom -> PromosAtom

Updates ricochet through the graph. That's it!

The point is, regardless of the implementation or the semantics, all of these atomic libraries have revived the concept of multiple stores and made them not only usable, but a real joy to work with.

The 6 reasons I gave for wanting multiple stores are exactly the reasons why the atomic model is so powerful:

  1. Autonomy - Atoms can be tested and used completely in isolation.
  2. Code Splitting - Import an atom and use it! No extra considerations required.
  3. Standardized Primitives - Anything can expose an atom for automatic integration.
  4. Scalability - Updates affect only a small part of the state tree.
  5. Destruction - Simply stop using an atom and all its state is garbage collected.
  6. Colocation - Atoms naturally define their own state, side effects, and update logic.

The simple APIs and scalability alone make atomic libraries an excellent choice for every React app. More power and less boilerplate than Redux? Is this a dream?

Conclusion

What a journey! The world of React state management never ceases to amaze, and I'm so glad I hitched a ride.

We're just getting started. There is a lot of room for innovation with atoms. After spending years creating and using Zedux, I've seen how powerful the atomic model can be. In fact, its power is its Achilles heel:

When devs explore atoms, they often dig so deep into the possibilities that they come back saying, "Look at this crazy complex power," rather than, "Look at how simply and elegantly atoms solve this problem." I'm here to change this.

The atomic model and the theory behind it haven't been taught in a way that's approachable for most React devs. In a way, the React world's experience of atoms so far has been the opposite of Flux:

Atoms in Theory vs Atoms in Practice

This article is the second in a series of learning resources I'm producing to help React devs understand how atomic libraries work and why you might want to use one. Check out the first article - Scalability: the Lost Level of React State Management.

It took 10 years, but the solid CS theory introduced by Flux is finally impacting React apps in a big way thanks to the atomic model. And it will continue to do so for years to come.

· 13 min read

cover photo

Original photo by trail on Unsplash

(Originally posted on HackerNoon).

In a recent conversation about state management in React, the topic of scalability came up. I was disappointed, but not at all surprised to hear it written off as an unimportant buzzword.

Having been a fly on the React state management wall for 8 years, I've seen this general sentiment expressed over and over: All popular React state management solutions are "scalable" and that's all you need to know.

It isn't.

Join me for a minute and I'll tell you why.

What Is Scalability?

I hate to be one of those blogs that starts off a lawn-treatment article by defining grass. But the very definition of scalability when it comes to state management has been lost in the React community.

Allow me to define it.

"Scalability" refers to a tool's ability to adapt to new challenges over time. Importantly, it goes both ways - scaling up and scaling down. A scalable tool handles every situation well - elegance in every facet of complexity:

Scalability = Elegance across App Complexities

This would be a very idealistic state manager. In reality, there is no such silver bullet. Scalability is the artform of getting as close as possible to this perfect plateau.

Elegance is largely subjective, but there are a few measurable statistics like performance benchmarks, lines of code (read boilerplate), and the number of unique concepts the user is required to know to do a given task. Still, some state managers will be more "scalable" for you. And that's fine! Just keep this in mind:

The Criteria

Apps grow. Teams grow. A scalable state management tool needs to elegantly solve a number of problems at every stage of an app's growth. These problems include:

  1. Performance. Bigger apps need to move data quickly and prevent unnecessary rerenders.
  2. Debugging. Are updates predictable? Can you track down unexpected changes?
  3. Boilerplate. Tiny apps don't need a bazooka and big apps don't want thousands of them.
  4. Side effects. Are these defined clearly? Are they flexible and simple?
  5. Caching/Memoization. Big apps need control over memory usage and tiny apps don't care.

I'll call these the 5 Great Scalability Criteria. There are more considerations like code splitting, testability, extensibility, interoperability, adoptability, even-more-abilities, and the learning curve. But these 5 will do for this article.

As a general example, an overtly simple state manager might look something like this:

good for small apps, bad for big apps

A naive solution becomes less useful over time, requiring you to find uglier and more complex workarounds. Such a tool can start to get in your way more often than it helps.

A truly scalable state manager doesn't drown a tiny app in config files and boilerplate and at the same time it doesn't leave a big app stranded in a sea of missing features and unstructured simplicity.

I'm sure this all sounds pretty straightforward. So why do I feel like this art has been lost?

React Context

Don't get me started. Nope, too late. React's new(ish) context API is a thing of beauty and a joy to work with, if you don't use it for state management (directly).

React context is the quintessential example of a non-scalable React state management solution. As an app grows, React context inevitably leads to performance problems and dev X nightmares like indirection, mystery updates, implicit dependencies, and lots of tightly-coupled logic that makes abstraction, testing, and code splitting difficult.

React context creates as much lock-in as a real state manager - and even more since you'll write the boilerplate yourself. All that added boilerplate also increases the surface area for bugs.

// There are many potential footguns with even this many providers - e.g.
// UserProvider creating an implicit dependency on AuthProvider, Routes
// rerendering due to UserProvider's state updating, etc.
function App() {
return (
<AuthProvider>
<UserProvider>
<RouteProvider>
<Routes />
</RouteProvider>
</UserProvider>
</AuthProvider>
)
}

I've seen this happen countless times on Twitter and Reddit:

Someone complains about how they regret starting with React context and asks what they should use instead.

Within a few hours, someone else will say, "Hey, I'm new. Where should I start?" and we all jump on and tell the poor guy to start with React context, dooming him to the same fate. This goes on in an endless loop.

Are We Ever Gonna Learn?

The advice to use React context for state management is not malicious. In fact, I believe it comes from good intentions. React devs tend to recommend React context for state management because it sounds like a decent, unopinionated, neutral, objective, diplomatic solution that's readily available.

And it is all of those things. But it's also really bad advice.

I know that React state management is a polarizing topic. Avoiding it sounds like smooth sailing.

React devs vs state management

But don't forget that competition breeds innovation. In the last 8 years, React state management has evolved in many, many ways that will benefit you. Don't ignore it!

To be clear, I'm not talking about useState and useReducer by themselves. Those are great tools for keeping local state tied to a component's lifecycle. You should use these even when you have a good state manager.

I'm talking about the moment when you need to lift state up (which you will need to do). Simple props are totally fine, as shown in that linked doc. But when you run into prop drilling problems, have a plan from the very beginning for what you're going to use. And don't use raw React context. Just don't. You'll thank me.

Doing It Right

Now, I'm sure you've heard that React context can be used effectively at scale. And that's true (to an extent), if you do it right.

The main principle for this is using React context for dependency injection, not state management. That article is an excellent breakdown of this technique. The gist is that React context is a very powerful low-level model that requires at least a thin wrapper to be used for state management.

The "thin wrapper" can be as simple as an RxJS BehaviorSubject.

Bad:

function UserProvider({ children }) {
const [user, setUser] = useState({})

return (
<userContext.Provider value={{ user, setUser }}>
{children}
</userContext.Provider>
)
}

Good:

function UserProvider({ children }) {
const subject = useMemo(() => new RxJS.BehaviorSubject({}), [])

return <userContext.Provider value={subject}>{children}</userContext.Provider>
}

This is just the (simplified) Provider. To use React context properly, you also have to learn to colocate logic, create hook wrappers, and create a system for triggering and taming rerenders using the pub/sub or similar model.

Sound like a lot of work? Good, because it is! To use React context properly, you essentially have to roll your own state manager.

If you're willing to tackle the 5 Great Scalability Criteria yourself, this is totally fine. Just don't go off the deep end with generators and stage 2 ECMAScript proposals. Keep concepts familiar for the sake of future team members and your own future self.

Alright, you knew Redux was coming.

Redux

I'm talking about Redux pre-RTK here. Due primarily to its infamous boilerplate, raw Redux has proven to not be a very scalable solution - it's clumsy for small apps to get started with and at the same time is clunkily verbose for large apps.

Redux did, however, make long strides over previous Flux implementations in other scalability criteria:

  • #2 Debugging - Redux's time travel model was revolutionary
  • #4 Side Effects - Redux's middleware finally gave these a comfortable home

We don't talk about Redux Saga

Redux Toolkit (RTK) finally fixed many of the boilerplate problems. This alone makes RTK a very "scalable" state management solution.

Redux excels in the moderate-to-fairly-high complexity range. RTK is an overall improvement, but is still a little verbose for very small apps. Overall, I'd draw its scalability like this:

RTK vs Redux

I'll cover that sharp drop at the end in a minute, but first:

Zustand

This excellent state manager removed even more of Redux's boilerplate at the cost of some debugging power. You can think of Zustand like a very simplistic RTK that scales down way better and scales up only a little worse.

I'll cut to the graph:

Zustand vs Redux

Since Zustand covers the full spectrum better, altogether, I would call Zustand more "scalable" than RTK. It's more comfortable for small apps to use and will be a steady companion all the way up almost to the extremes of complexity.

I've seen many people on the RTK train bewildered about Zustand's success when RTK clearly scales up better. I hope the above graph clears this up a little - many apps don't need to scale up that far. But they do want a state manager that scales all the way down to elegantly handle state in even the initial PoC/prototype phase.

Alright, it's time to talk about those steep drops at the end.

Singleton vs Atomic

Redux and Zustand use a singleton model. You create one store. All side effects and state derivations (selectors) hook into that store.

This approach has a hard upper limit. I wrote some thoughts about this on this Reselect discussion. The gist is that an app with lots of fast-moving data can start to bottleneck a store with lots of subscribers, reducers, and deep selector graphs.

Recoil introduced a new pattern for storing state and propagating updates. This atomic model has proven to scale up better than the singleton model at the cost of some hefty learning curves.

Still, it can be very useful in even very small apps. I'd draw its overall scalability something like this:

Recoil vs RTK

That slow tail off at the end is 👌 and is why I believe atomic (or similar) models are the future.

Jotai

Jotai did to Recoil what Zustand did to Redux; it sacrificed a little long-term power for some short-term elegance. This makes Jotai an excellent all-around tool.

The graph:

Jotai vs Recoil

Please remember that this is just a dude drawing lines on Excalidraw. These lines don't accurately reflect each tool's capabilities in every aspect of every app or in the hands of every programmer. I'm trying to communicate only the general "scalability" of these tools from my experience given the scalability criteria I outlined.

With its simple APIs and very gracefully declining performance and capabilities, Jotai is the closest any of these tools have come to the "perfect plateau" at the beginning of this article.

Yes, that's right. Jotai is the most scalable state management solution. The end.

Well alright, almost the end. I wouldn't be here if there wasn't a little bit more to the story:

We Need To Go Deeper

After encountering the hard limit of the singleton model, a few coworkers and I started studying Recoil and Jotai. We loved the concepts and the performance scalability, but determined that they were lacking in other scalability criteria.

In 2020, I hatched a plan for an in-house solution that would sacrifice only a tiny bit of Jotai's simplicity in exchange for a much more powerful atomic model. Designed from the ground up with the 5 Great Scalability Criteria (and a lot more) in mind, this model scales up better than any of its predecessors and scales down only a little worse than Jotai.

This new tool started driving our production apps in early 2021 and has been a lifesaver in the extremes of complexity. 2 years later, we have finally open-sourced this tool as "Zedux".

Zedux

Besides solving better for all of the 5 Great Scalability Criteria, Zedux added many features over Recoil/Jotai like React Query-esque cache management, cross-window support, real Dependency Injection, evaluation tracing, and atom exports. These are fun to say, but I'll write more about them in a separate article.

Overall, by design, Zedux's scalability looks like this:

Zedux vs Recoil vs Jotai

Besides a clear bias and my own experience using Zedux in data-intensive applications, the reason why I draw Zedux's scalability so generously is because of the 5 Great Scalability Criteria:

  1. Performance (benchmarks here, here, here, here)
  2. Debugging
  3. Boilerplate
  4. Simple but completely powerful side effects model
  5. Cache control features

There are many more reasons, but I'll limit it to these 5 for this article. Zedux was designed primarily to manage extremely volatile state in big fintech applications and to scale down fairly well too. But that doesn't mean it does everything perfectly.

For small apps, Jotai and Zustand are certainly great options. Beyond that, personal preference can also come into play. As I said at the beginning, some tools are more scalable for you.

Does Scalability Matter?

Scalability is a general principle, but that doesn't mean that every extreme applies to every app. You are free to analyze your own application and determine which parts of this discussion matter and will ever matter for it. Just be sure to leave yourself plenty of leeway in your predictions.

Anything with live-time data visualization may need to scale up performance-wise beyond what the singleton model can handle. Additionally an app that's "just very, very big" might need more power.

This is all up to you to decide. But no, not all of this applies to every app.

Conclusion

All modern state managers are solid options for most apps, but they scale up and down differently for each of the 5 Great Scalability Criteria.

Pick one you like that will suit the relative size and future complexity of your app. And don't use React context for state management unless you really want to make a state manager from scratch.

If the decision is too hard, here's my recommendation as someone who's been using React for 8 years:

  • Pick either Zustand or Jotai if you want something you can start with quickly and that will scale up to moderate complexity (think an e-commerce or forum app).
  • Pick RTK if you want something that will scale up very well to almost the extremes of complexity and has a solid community.
  • Pick Zedux if you want all your bases covered - from simplicity to ultimate complexity and everything in-between.

Lastly, I know that I've omitted many great tools like XState, React Query, and SWR. These tools are utilities that are very scalable in their own right, but aren't full replacements for a good state manager.

As for other global, atomic, proxy-based, and signal-based state managers, I'm sorry I couldn't get to them all in this article. Feel free to comment or start a discussion in the Zedux repo for better details or more comparisons.

If you get nothing else from this article, just remember that applications grow. Plan for growth and you'll succeed.

· 12 min read

Zedux is a molecular state engine for React. After years spent as proprietary software hidden in a private GitHub repo, it's officially open-sourced and version 1.0.0 has been released!

· 5 min read

Zedux: A Molecular State Engine for React is officially open-sourced and version 1.0.0 has been released!

Check it out on GitHub here. Check out the docs here. Check out the introduction here. Check out the quick start here.

This article will give a brief history of Zedux while trying to stay high-level. If you like your announcements detailed and mixed with a little controversy, check out the follow-up article, "Zedux: Is this the one?".