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.

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:

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

⬅️ Previous Post
Back to top