React

Since 2018, JavaScript UI libraries have undergone a notable transformation. It touched nearly every major framework — React Hooks, Svelte 3, Vue Composition API, and even Ember Octane all felt the ripple effects.

For some developers, this shift was jarring and unwelcome. But for many others, it felt like the missing puzzle pieces clicking into place. Curiously, despite the surface-level differences, these libraries seem to be gravitating toward a common destination. Never before have they looked so alike.

If this sounds unfamiliar, take a look at Finding Fine-Grained Reactive Programming I wrote earlier.

That said, this trend isn’t strictly about reactivity. React Hooks, for instance, aren’t inherently reactive. And when I talk about reactivity, I don’t mean the entire spectrum of reactive programming — stream-based systems like RxJS share similarities but aren’t quite the same thing.

The pattern I’m pointing to is fine-grained reactivity: architectures that track reactive atoms and propagate their changes by wrapping execution in computed expressions.

What unites these approaches is their lineage, tracing back to functional programming ideas developed in the 1980s. Yet we’re only now seeing them hit the mainstream in JavaScript over the last few years.

Functional programming has always been a tough pill to swallow. Most of us begin writing code to express an idea long before we catalog the tools we have at hand. You might use Monads or Lenses regularly without ever diving into the theory behind them. And I’m not convinced the typical developer needs to. It’s already hard enough keeping track of the dozen or so operators you need to be productive with RxJS.

Still, here we are. And at its core, this is really just an extension of a simple idea:

let view = fn(state);

If a large chunk of our work involves shaping data into an interactive view, it’s not hard to see why patterns emphasizing data flow and transformation would gain traction. That’s why they keep resurfacing.

React

Let’s turn to React first — a library that isn’t reactive. React Hooks acted as a catalyst for the wave of changes that followed over the next couple of years.

On the surface, Hooks don’t look all that different from Component lifecycle methods. Except they let you register as many as you like. It’s the difference between onclick and addEventListener. More importantly, they enable co-location of data manipulation, which opens the door to abstraction and encapsulation.

This declarative data pattern makes composition possible in a way that mirrors how you compose views from components. In effect, React introduced new data primitives for building applications.

The consequences are significant. Though Hooks are confined to components and entirely opt-in, they’re rapidly displacing older patterns. It’s not out of necessity — it’s that once you work with these primitives, you’re on that path. They’re deceptively invasive.

Developers quickly noticed that pairing the useReducer hook with the Context API could mimic Redux. That kicked off the debate over whether Redux still mattered.

But Redux isn’t actually the React state library most threatened by this shift. React Context has pitfalls when the Provider’s parent manages updates, as it forces re-renders down the entire tree. Redux avoids that trap with its connect HOC (Higher-order Component).

No — if anything, the writing is on the wall for MobX, React’s most popular reactive library. Not that MobX isn’t useful, but look what happens when you switch to Hooks. There’s a clear parallel:

observer => React.memo
observable => useState
computed => useMemo
autorun => useEffect

Or put another way:

// React atom
const [count, setCount] = useState(0);

// React memo(derivation)
const double = useMemo(() => count * 2, [count]);

// React effect(reaction)
useEffect(() => console.log(double), [double]);

// update atom
setCount(c => c + 1);

It doesn’t matter that these operate entirely differently. And it doesn’t matter that MobX does auto-tracking while Hooks are explicit. You find yourself writing code in the same style, composing behaviors the same way, until you start questioning whether the library is worth the dependency. Especially once you see what MobX is actually doing in the context of React.

React executes top-down, building a Virtual DOM representation and efficiently diffing and patching the real DOM. MobX inserts itself into that flow, breaking apart the tree to create a secondary reactive graph that updates individual components — which still produce Virtual DOM nodes that get diffed and patched as usual. Reactivity becomes pure overhead on top of a Virtual DOM that’s already designed to update that way. Within React, nothing outperforms React’s own update cycle.

So it’s no surprise that new state solutions have emerged to tackle this. Most notably, Recoil, a library from a team at Facebook, uses state atoms and selectors (lenses) — which again map to reactive libraries and update at the component granularity.

// Recoil atom
const count = atom({  key: 'count',  default: 0});

// Recoil selector (derivation)
const double = selector({  key: 'double',  get: ({get}) => get(count) * 2});

// reaction (React always re-renders)
const doubleValue = useRecoilValue(double);
console.log(doubleValue);

// update atom
const setCount = useSetRecoilState(count);
setCount(c => c + 1);

It’s not React’s official state solution, but we don’t need one to see how MobX’s key advantage has largely been addressed. Its weakness now is the overhead it adds in most of the systems emerging from the “new” React. And React isn’t finished moving in this direction either. It should become clear what tradeoffs come from mixing a VDOM with reactivity. Which brings us to…

Vue

Vue is another framework that gives React a run for its money, and its response to hooks has finally brought their reactivity system into the spotlight. I say “finally” as someone who's passionate about reactivity. From the wider community's perspective, they've actually been quite swift. But for me, from the moment I laid eyes on Vue, I was hoping they would. I recognized that, in the interest of approachability, they intended to keep it under wraps.

Back when Vue was gaining traction, reactivity had a bit of a bad reputation. Early reactive UI libraries such as KnockoutJS were quickly losing ground due to their dependence on “magic” and their well-known unpredictability.

Vue's dedication to simplicity would never permit them to pursue that route. It's simply inherent in its character, having grown in popularity by toeing the line between borrowing the best features from other libraries while presenting them in the most user-friendly way. Vue, by its very design, strives to be a safe library, which makes it tough for it to stir things up.

Now, however, Vue 3 is introducing the Composition API, and this represents a major leap forward. It's constructed on a genuinely reactive approach leveraging proxies, and I've thoroughly enjoyed observing its evolution. It's still early stages for identifying the optimal patterns. For those new to reactivity, this could be the most accessible library, covering all the essentials.

It does come with its own unique quirks. Take their ref objects, which are meant for simple atoms but also function as deep proxies when holding objects. This default certainly ensures that updates always happen, but it also introduces overhead when handling straightforward tasks. There are utilities like toRef that make the transition from the proxy-based reactive back to ref effortless. Yet, all of this is intended to offer a consistent API where everything is a proxy.

This leads to a certain level of verbosity. While many libraries rely on function calls, Vue appends a .value at the end, which can't be renamed or given an alias. Even so, Vue is still in its learning phase, so it's well-positioned for people to learn right alongside it.

// reactive atom
const count = ref(0);

// derivation
const double = computed(() => count.value * 2);

// reaction
watchEffect(() => console.log(double.value));

// update atom
count.value++;

They're also looking into more refined update mechanisms, which is a positive development. However, they continue to carry the load of their Virtual DOM. It's uncertain if they'll alter their strategy, as Vue tends to prefer the middle ground, and this offers them the most versatility. When I put this question to Evan You, the creator of Vue, on Twitter he confirmed as much:

Because Vue allows you to write manual render functions and mix them with template based components. Giving up vdom is giving up an important capability for advanced use cases where logic expression is more important than a bit of perf.

Vue 3 remains a substantial improvement, as they've cut size by nearly 33% and boosted performance by a similar margin over Vue 2. It's remarkable for a framework with this level of popularity to achieve such significant gains. They've even succeeded in getting TypeScript to cooperate within their custom template DSL.

Svelte

Another noteworthy framework is Svelte, which has really found its footing this year. It has secured that highly sought-after 4th position for UI frameworks in most comparisons. It's arguably the framework generating the most buzz, and that's largely because of its distinctive approach as a compiler. Its capacity to build animations directly into its template syntax remains a standout feature, alongside its knack for producing incredibly tiny bundle sizes.

Writing code with Svelte feels like working in its own dedicated language:

// reactive atom
let count = 0;

// derivation
$: double = count * 2;

// reaction
$: console.log(double);

// update an atom
count++;

You won't be able to achieve this with fewer lines of code. Svelte's conciseness is genuinely impressive. That said, it does come with certain limitations.

In the absence of proxies or function calls, there's no grammatical distinction between the reactive atom and its contained value. When assigning, it's clear we're interacting with the atom. But elsewhere, we can only infer that we're reading the value.

This means everything stays contained within the component. Features like stores depend on an entirely different syntax. This, in turn, actually reduces the opportunities for writing less code as your application grows, and it can become limiting.

Svelte aims for the smallest possible runtime, which, ironically, means embedding more code in each component. This has been shown to scale less effectively than most frameworks, though not, in any statistically meaningful way.

Svelte might just be the best demonstration framework we've seen thus far. For a single component, it demands the least amount of code, generates the smallest bundles, and delivers solid performance to boot. That's not to say it can't handle large applications. But it knows how to make an impact in the majority of demos.

Solid

And now is a fitting moment to discuss Solid. Each framework mentioned so far has adopted a different stance on reactivity. I'd like to highlight another framework with a somewhat different perspective, because I believe it embodies the leading edge of reactive UIs.

Solid has been leveraging features for years that other frameworks are only beginning to explore. This includes more finely-grained templates and compiled JSX. It demonstrates that a VDOM isn't necessary to harness the complete versatility of JavaScript.

Solid draws from functional programming paradigms like React, uses a compiler similar to Svelte, and still employs proxies like Vue. However, the primary distinction between it and the others is that Solid's architecture isn't anchored to its components but rather to reactive scope itself.

No matter how you organize your code or divide your components, at runtime it all condenses into a single reactive graph. The compiler only manages the JSX transformation, akin to other reactive template DSLs. The clear advantage here is that syntax highlighting, tooling, and TypeScript support come almost effortlessly.

// reactive atom
const [count, setCount] = createSignal(0);

// reactive memo(derivation)
const double = createMemo(() => count() * 2);

// reactive effect(reaction)
createEffect(() => console.log(double()));

// update atom
setCount(count() + 1);

Solid also offers proxy support, but you don't need any special helpers—just wrap things in a thunk () => state.count. In most cases, you don't even worry about derived values since wrapping with a thunk is all that's needed. This removes a significant amount of both written and cognitive reactive overhead.

The render system evaluates props lazily, which means components are temporary factory functions whose sole purpose is to create closures over the state. They effectively vanish after execution, similar to Vue's setup function. The state exists only within the reactive computations that rely on it.

This means Solid is considerably more performant than the other options. Solid's renderer has been applied to other reactive frameworks like Knockout, MobX, and Vue, showing similarly impressive results (ko-jsx, mobx-jsx, and vuerx-jsx). This is an approach that fully commits to reactivity and doesn't attempt to conceal it.

Exploring the state of reactivity patterns in 2020 — figure 1

Sampling from JS Framework Benchmark — May 2020

Solid does have its own eccentricities. It nearly dogmatically insists on read/write separation and unidirectional data flow, even at the expense of user-friendliness. Its proxies can't be updated directly either; they function as an immutable tree. The setState method generated at creation supports paths similar to ImmutableJS and acts as a lens for updates. Although, for those who rely on mutation, there is an Immer-inspired form available.

The key point is that, like React, it uses array tuples to prevent naming collisions and allows read and write capabilities to be passed independently. With only 2k stars on GitHub, it's certainly the underdog in this crowded ecosystem. But its performance and footprint are eye-catching, and it offers a feature set comparable to the others (though perhaps fewer than what you'd find in Angular or Vue).

Conclusion

Reactivity remains a shifting landscape in 2020, and there are plenty of options to choose from. From steering clear of it entirely with React, to going all-in with a renderer built entirely on it with Solid, there's a broad spectrum. There's Vue, which is beginning to lean into its true nature, and Svelte, which wants you to set all this aside and reconnect with the joy of programming. This topic is sure to stay active, with a wealth of innovation happening in this arena.

Some important trends to keep an eye on:

  1. Expanded capabilities of Template DSLs. Look forward to enhanced tooling support and TypeScript integration in the near future. JS DSLs like JSX, once thought impossible, are now feasible, introducing a completely new level of flexibility to this field.
  2. Broader adoption of compilation. Svelte's animations come to mind, but there are countless applications. Compilation serves as the ideal counterweight to the typically heavier setup cost associated with reactivity.
  3. Performance and adaptability of granular methods. This isn't only about achieving the finest-grained updates. Frameworks in this category are exploring ways to manage reactive boundaries to minimize creation and synchronization overhead.

I wholeheartedly suggest spending some time looking into at least one of the frameworks discussed in this piece to get a feel for these patterns. Learning a single one is sufficient. In many respects, they share a lot of similarities. Without question, we're experiencing the most fundamental transformation in Web UI development over the past half-decade.