The Absolute State of Management
It is by no means the gravest linguistic crime of the React epoch,1 but "state management" grates like nails on chalkboard. A Socratic dialogue I keep having about it goes something like this:
me: This UI is far too heavyweight for what it does, have you considered not using React?
them: But how will we do state management without React?
me: What do you mean?
<alternatives>are reactive.them: React manages state in our app.
me: It does? Then what are
<various_libraries>for?them: Oh, those manage state.
me:
The libraries that trigger this "Who's on First?" routine include:
-
MobX, which promises "simple, scalable state management."
-
Satchel, apparently, "is characterized by exposing an observable state that makes view updates painless and efficient"
-
Recoil, billed as "a state management library for React"
-
Jotai, which pledges "primitive and flexible state management for React"
-
XState, a "state management and orchestration solution for JavaScript and TypeScript apps"
-
Apollo, promising "a comprehensive GraphQL state management library for JavaScript"
-
Redux, tagline: "a predictable state container for JavaScript apps."
-
TanStack Store, "a framework-agnostic state primitive for immutable updates"
-
Zustand, described as the "🐻 bear necessities for state management in React"
Contrasts sharp enough to cut softened butter.2
Suffice it to say, the ecosystem has not picked a winner. Indeed, I regularly see several of these libraries on a single page. But even one would seem too many, given that we're adding a second "state management" system to a framework that spills considerable documentation ink chest-beating about its state management facilities.
One of the following must be true:
-
React is a state management system and someone has smuggled in a duplicative dependency we can jettison.
-
React is an inadequate state management system and we should stop trusting it with an ill-defined subset of the "state management" problem.
-
React is not a state management system and we should stop pretending otherwise.
But in the to-and-fro of consulting engineering one can't just say as much, for fear of offence against frameworkist articles of faith.3
Thankfully, this is a blog post, so we can cut to the chase and admit that none of these tools manage state.
They might encapsulate state. They might even filter or transform state updates. But they do not, in any meaningful way, manage it.
Instead, they locally propagate notifications about state changes and, in some cases, post-process them or serve as caches.4 Conceptually, they are expressively equivalent to event buses and pub/sub systems, which are better understood as message passing backplanes.5
To understand the difference between state propagation and state management, we need to add the time dimension. React-centric "state management" libraries handle time the way C++ does for multithreaded code — which is to say, it's left as an exercise to the reader.6
Systems without a built-in conception of time and ordering can be subject to temporal effects. They can even synthesise compound state out of unordered, un-timestamped updates. But they occupy a world of hopes and dreams when it comes to asserting affirmative management of state because managing state means correctly applying changes over time and resolving conflicts.
Then and Now and When
A large fraction of the problems from overgrown React and Redux attempts at "state management" spring from the same wellstone: updates to the state propagation systems come from a variety of sources, with varying frequency and specificity. Pure propagation systems crumble under the incidental complexity that results from trying to sort out the consequences with only an event bus abstraction. It becomes unclear whose job it is to sort out which updates are "correct", and optimistic UI updates become a liability — or at least a bun fight.
At bottom, this is the same problem as managing data across multiple machines, and managing data over the network is the same as managing it across time that spans from the past, to the current moment, as well as the future.
This insight is important because it allows us to consider what's currently in memory vs. the same app rebooted at some future point without losing our minds. A future local instance of an app is conceptually the same as the app booting on an entirely different computer. Getting crisp on this is the key to throwing out jejune notions of "state management" and replacing them with tools that are fit for purpose.
Systems that manage state — rather than merely propagating or filtering it — have built-in concepts of time and order, and this allows them to handle both offline state and online real-time collaboration naturally. Instead of hoping that everything is happening in some window that we can squint at and call "now," true managers of state mutations have a way to reorder and resolve conflicts, and through that coherence, build a reliable and high-performance view of data changes over time.
You can distinguish them from state propagation tools by looking for variations of vector clocks in their internals. Time and order metadata attached to mutation enables global ordering, and we can't truly manage state without it.7
This conceptual break also provides an answer to our fish-or-fowl question: none of those libraries actually manage state; React included.8
Getting Serious
Now that we can more accurately describe what these systems do (and don't do), the phrase "state management" mostly reads as confusion. But hold on to some grace; the folks deploying jumbled terminology come by it honestly.
Systems that really manage application state are not yet popular, and most teams are not familiar with the algorithms and systems that do it credibly. Discussions of CRDTs, Operational Transforms, Prolly Trees, and related approaches to consistency are still rare in the frontend discourse.
Thankfully the gap from upgrading our understanding of theory to deploying better patterns is small. Practical implementations are readily available; each have strengths and weaknesses depending on use-case, but all manage data in a time-aware way:
-
Y.js: CRDTs meet text editing. Thanks to breakthroughs in data structure performance, a full CRDT approach is dramatically more practical than it was just a few years ago.
-
Zero: the successor to Replicache, Zero targets live collaboration in addition to data sync. OSS'd or hosted, it's what I'd start with on a new project today.
-
Fluid: Microsoft's in-house, OSS'd sync and collaboration system that takes cues from OT.
A beautiful, generative outcome of these systems is that because they think about time and conflicts, they're perfect for building systems that gracefully handle both edges of "now", enabling real-time collaboration seamlessly while simultaneously lighting up offline-first styles of development.9 Some (e.g., Y.js and Replicache) go far enough in this direction to enable true local-first development.
Regardless of which mechanism you adopt, these systems are a sea change. Data models stop being amalgamated hopium, and both offline use and realtime collaboration become possible as a side effect of getting the ordering of data mutation application correct.
This is the future we can have today. All we need to do is admit that "state management" isn't, then decide to do something about it.
FOOTNOTES
By my count the worst linguistic misapprehensions spread by React's know-nothing culture are probably "server-side rendering," "pure function," and "concurrent." Each one camps out on a previously well-studied concept, re-defines it in a less-than-rigorous way, then flicks at the larger (more valuable) idea in order to justify a cheap knock-off.
And that's to say nothing of the swindle being attempted through the renamings Reactors have given to the MVC bits of the React ecosystem. The very obvious MVC-with-global-variables patterns were marketed in an aloof, functional-programming-ripoff attempt to have something new to say when in fact they were just the old thing with more transpiler overhead.
It's mank, and serious engineers are universally confused or pissed off by these cheap marketing theatrics. ⇐
Given the hulking payload of Apollo, you'd be forgiven for wondering why it does not simply eat the other eight. ⇐
Because it's generally unproductive (not to mention, kind of a dick move) to simply tell someone that their understanding of state management is entirely backward, the best I usually muster in conversation is heightening the contradictions by proximity.
I wish that putting React's claims about state management next to those from other libraries triggered the same sorts of questions in others that it provokes in me, but the hit rate on this technique is dismal.
Most developers only really seem to upgrade their understanding of what's possible when they look to add realtime collaboration to their systems, which is both understandable and somewhat frustrating, given the amount of effort that goes into pressing Apollo and Redux into service as ill-fitting sync engines. ⇐
These systems might be caches, after a fashion, but they aren't databases, as they don't provide any method for querying data. Most also fail to address local persistence. The kludges I've seen to add storage to them further confirms they are not suited to any sort of scalable access role. It will always be a fool's errand to abuse a thin slice of a data layer snapshot as a generic access abstraction when the structure and metadata needed to do better than
O(n^2)are absent.Major products have limped along in a self-degraded state, punishing heavy users thanks to approaches that wouldn't pass CS101 evaluations of their scalability. Huge apps have been caught trying (and often failing) to boot from JSON serializations of Redux.
The fact that it ever worked — even on a developer's beefy machine — is testament to the ingenuity and dedication of hardware engineers, rather than any positive attribute of contemporary software pedagogy or frontend culture. ⇐
A single-host instance of RabbitMQ would be just as expressive and generally perform better. ⇐
Derogatory.
For those not versed in both languages state of play, C++ is actually more capable regarding data access and updates over time in a stuctured way than JavaScript; it's just buried in C++11 Memory Model arcana.
JavaScript, meanwhile, provides neither syntax nor standard library facilities for thinking about the time dimension of execution, despite continuing to slice aspects like program and class initialisation even more finely to accomodate reasonable defaults and their predictable overrides. ⇐
The most common objection to the complexity of vector-clock-based update systems is that it isn't necessary in many cases to be able to distinguish now from then or when.
This is (sometimes) true!
Many UI components have internal state data that isn't complex enough to need accounting for mutations over time. Those components also don't want to integrate deeply into a single data management pattern, and prefer to handle changes through an out-of-time state machine, or a state chart without history. That's often fine.
What's less fine is for us to pretend that once we escape the boundaries of these smaller components that we can muddle through with systems that attempt to extend the temporal void to conflicts that arise in application-wide contexts.
These are fundamentally distinct concerns, and the React-ish belief that we can think of everything as an infinite regression of components without cross-cutting architecture is falsified by the practices of the React community itself.
Said differently, Redux is a cry for help, and we have solutions that enable us to think more appropriately about how to quench the pain it introduces. But to get there, we have to step out of the void and into the stream of time. ⇐
We can't in good conscience call these libraries "state management" tools, but I don't have a better name off the top of my head.
Possible alternatives:
- Event blenders
- State strainers
- Model muddlers
- Mutation mixers
- Modification marshes
- Component complexifiers
- Metamorphosis mystifiers
- Confusion caches
- Transform tarpits
- Heap hodgepodges
- Update imbroglios
- Status SNAFUs
If you have a better suggestion, drop a note on Mastodon or bsky. ⇐
Honourable mentions for systems that try to deal with sync in a somewhat less structured way:
- PouchDB: a classic of the genre, the library is continuing to get updates that remove cruft. Unfortunately, the conflict resolution strategy it adopts is too naive to recommend for new production work.
- RxDB: unfortunately, RxDB leaves conflict handling as an exercise to the reader. Thankfully it does have a CRDT integration story, which is how I would recommend holding it.
- Replicache: implements full state sync and good conflict control. It's also OSS'd, but somewhat unsupported by Rocicorp now. A good answer for folks who find Y.js too cumbersome but want full offline-first (or even local-first) development with an authoritative store in the cloud.
Lastly, I would be remiss to omit that Google's Docs API is, in effect, an externalised entrypoint to the full live-editing OT system that powers Google Docs and Docs Offline. Unfortunately neither the server nor the client implementations of Google's modified-OT system are OSS or self-hostable, making it impossible to recommend. ⇐