JavaScript Reactivity (Part 1): A Global Look
Published 2026-05-08, About 8 minute read.
My world lately at work has been implementing a platform that supports product
groups bringing in their own web components. These web components are just the
entry point, and often we can see React, Lit, Ember, or Svelte running
alongside each other. It's part of my job to help with the services layer.
It's my baby. And that means each of these frameworks needs to share a common
kind of reactivity.
It's fun, it's tough, it's fascinating, and it's taught me a whole lot about
JavaScript reactivity, front-end frameworks, and agnostic patterns.
I want us to get a lay of the land in this post - understand where common
frameworks and reactivity libraries lie in relation to each other based on
three dimensions: depth, granularity, and directionality.
-
Depth: Are we concerned with paying attention to the whole
tree or graph of state? Or do we only care if the reference we're given is
different? Do we know when a leaf node in our state tree changes?
-
Granularity: Does the reactivity pay attention to
individual values or references? Or does it only care about whole objects or
services? Or is it even more coarse than that?
-
Directionality: Do we get pushed a notification when
something has changed? Do we sit back and wait for rendering time to pull
and then determine changes? Is it a combination of both?
Let's get a sense of where the most common frameworks and libraries are in
this diverse landscape. My hope here is to lay out a comprehensive framework
(pun intended) to categorize front-end reactivity, compare strengths and
weaknesses, and understand how frameworks converge on similar solutions.
Visualizing reactivity strategies 📎
At work we offer a service layer that provides dependency injection of
singleton services which each might contain state. Consumers need to react to
state changes, so we have the concept of "notifications" across servicices and
their consumers. It's a "meta-framework" of sorts- a runtime platform.
If service state changes, we want to notify all of our consumers. A
notification is pushed, but, long story short, the consumer has to investigate
and understand what has changed and react accordingly. In React and Lit
worlds, two libraries that are coarse-grained and utilize pull reactivity,
this is not a problem. In the Ember Octane world, which is push-based and
fine-grained, this was a headache.
This got me thinking about libraries along these axes. Reactivity systems
don't all play well together, and they vary in a lot of ways - which makes
writing adapters between them annoying. With Claude's help I plotted some
common libraries on a graph: granularity on the x-axis, depth on the y-axis,
and push/pull as color. The result is an image of where each library lives.
Click on the circles to see analysis of their placement.
Anatomy of reactivity 📎
Reactivity has two parts: the notification and the update.
The notification is fired letting participants know
something's changed. This is where granularity and depth are
determined. In wc-services, for example, there is an explicit notification
that gets sent to all service dependency chains. The catch, though, is that
service notifications are deduped, and so the consumer gets a notification but
can't tell from that alone what actually is the source of the change. This is
coarse reactivity. This is similar to what zone.js does. Zone.js'
notifications come in from every async event that can happen, which means that
it is being notified all the time, but it has to do the hard work of then
going out and figuring out what actually changed.
The notification also is closely tied with the depth of the reactivity.
Basically, at what level will a change trigger a notification? This isn't as
cut and dry as you would think. In wc-services, for example, there is a setter
for some fields on services that will notify as soon as a particular field is
overwritten. This might be slightly fine-grained, but it's coarse because if
you are storing an object on that field, changes of nested properties within
that object won't trigger this notification. And then to top it off, you can
call this.notify() manually to trigger notifications so you can
manually make up for deeply nested value changes. Zone.js has it worse,
though. A notification can happen
even if there isn't a change in state. At least in most reactivity
notifications like this there is at least a guarantee that something has
changed.
Then there's the update. When you notify, you have to do
something to respond to the changes and keep state in sync. This is largely
what determines if a strategy is push or pull. Wc-services works the best in
pull frameworks, because we can simply ask the framework to diff and
re-render. Frameworks like React or Lit do this extremely efficiently.
Signals-based libraries like Solid and Preact Signals push invalidations when
a value is written, and then effects pull the current value when they re-run.
Other push-based systems do all the work in reaction to the notification. For
example, Svelte 4 wires in the view changes at compile time, meaning that
Svelte pushes particular updates in the view when a change notification
occurs. The same happens with Solid, and partially with Preact signals. Solid
makes fine grained reactive changes to the view when a signal is notified, and
it doesn't call an overall re-render of the system. It does make the
fine-grained updates within the view, though. Preact Signals can opt into
this, but otherwise still participates in virtual DOM diffing like React.
If your eyes are glazing over a bit I don't blame you! Describing all these
systems, and trying to keep the descriptions accurate, is really hard and
wordy and I start to sound like my obsessed 9-year-old talking at length about
every single detail of all the blocks and properties of builds in Minecraft.
(Not that that's ever happened 🤣)
Still, there's a reason each one of these systems exist, and it's fascinating
to me how nuanced and varied reactivity can be in the same platform, and the
different categories and patterns that emerge.
Four quadrants emerge 📎
Splitting up the 2D quadrant graph we have four major categories of
reactivity:
-
Reactive Graphs: this category tracks properties on graphs
of objects deeply. Most of the time, recursive proxies are used to track
what is actually being referenced from within the graph of state, and then
highly efficient checks can be done on state changes to see what needs to be
notified.
-
Atomic Primitives: a collection of more generic state
mechanisms that can be used more agnostically. The interesting thing is that
most of these primitives are containers that wrap a value (an applicative
functor, if you will!) and expose the value through
.value.
They also tend to use getters or proxies to signal when a change has
happened.
-
Diffing: most famous in this group is React since they
basically started this category. Replace the state, render the DOM graph
virtually, and compare the new graph with the old one. At first, React was
criticized that this was quite expensive, but over time people experienced
the fact that this diffing was performant and efficient... mostly!
-
Wholesale discovery: there are groups of reactivity where
notifications are sent with limited information about
what exactly changed. For one reason or another, this is done to
avoid computing notification graphs, avoiding cycles, and de-duplicating
notification signals. In the end, this means that the framework or change
detection needs to discover what actually changed. Arguably the least
efficient, but probably the most easily shared agnostically across a
platform.
Libraries vs. frameworks 📎
I don't mean to start a fight about
what makes a library a framework. I just want to point out that there are libraries that don't have strong
rendering preferences, and they fit on this graph as well. Valtio, MobX,
Nanostores, XState, Zustand, Redux, Cycle.js, Immer, event buses... MobX, for
example, is a Reactive Graph just like Vue, but it leaves the view layer up to
you.
These libraries' main goal is to hold state and/or notify of changes. They are
often developed within particular frameworks, but always assume they function
in a system that allows consumers to discover state changes in a systemic way,
and inform the framework or view of those changes.
You'll note that "event bus" is in the graph, but this is the oddest oddball
of all. Events don't hold state. Yes, technically, they can pass payloads of
data, but they are not long-lived. All others on the graph hold state and
track changes over time. But I find that event buses are an option and
sometimes used, so we should include them.
Up next: depth 📎
In Part 2 (Deep vs. Shallow Reactivity) we'll dive into
depth: what it means, and the strategies for tracking state changes from
shallow all the way to the deepest, most nested state graph.
If you like my work, please let me know! Find me on Bluesky or Mastodon. I also have an RSS feed here