We’ve seen a drastic shift in JavaScript UI library APIs and patterns since 2018. It has affected almost every major library in some way, from React Hooks, Svelte 3, Vue Composition API, to even Ember Octane.

For many users, this was an unexpected and unwanted change. But for many others, it was like the pieces finally fit. And strangely enough, no matter how different the library, they all seem to be converging on this point. Things have never looked so similar.

Not sure what I’m talking about? Check out Finding Fine-Grained Reactive Programming I wrote earlier.

Now this trend we are seeing isn’t strictly reactivity. React Hooks are not innately reactive. And even when talking about reactivity I’m not referring to the full scope of reactive programming as there are systems using streams like RxJS which are similar, but not exactly the same.

The pattern I’m referring to is fine-grained reactivity, which describe architectures that involve tracking reactive atoms and the propagation of their change through wrapping their execution in computed expressions.

What they all have in common are their roots in functional programming approaches that have been developed since the 1980s. But we are seeing them reach primetime in JavaScript libraries the past few years.

Now functional programming is always a bit of a hard to digest. The reason is most of us start writing code to express an idea long before we’ve catalogued the tools at our disposal. You probably use Monads or Lenses daily but didn’t stop to learn a bunch of theory behind them. And I am not sure the average developer needs to worry about that. It’s hard enough to keep track of those dozen or so operators you need to know to be effective with RxJS.

Yet we are here. And it is really an extension of a simple concept:

let view = fn(state);

If most of our job is the transforming data to an interactive view it isn’t hard to see patterns that focus on data flow and transformation to be desirable. It is why these patterns keep showing up.

React

Let's start digging into the state of reactivity in 2020 with React, a non-reactive library. React Hooks acted like a catalyst for the whole set of changes that would follow the next couple of years.

Hooks are not that different than lifecycle methods for Components on the surface. Except they offer the ability to register as many as you want. It’s like the difference between onclick and addEventListener. But more so, it allows co-location of data manipulation and in so the ability to abstract and encapsulate it.

This declarative data pattern allows composition in a very similar way you compose the view out of components. React had effectively introduced new data primitives to construct your applications with.

This has significant repercussions. While hooks are localized to components and completely opt-in they are quickly replacing older patterns. It isn’t because they need to, but because once you are working with these primitives you are on that path. They are deceptively invasive.

People pretty quickly realized that by using the useReducer hook with the Context API they can do an impression of Redux. This started the whole discussion about whether Redux was still relevant.

But Redux isn’t actually the React state library most at risk with this change. React Context has its weaknesses when using the state of the Provider’s parent to manage updates, since it requires re-rendering all the way down the tree. Redux does not suffer this weakness with it’s connect HOC (Higher-order Component).

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

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

Or in other words:

// 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 work completely differently. And it doesn’t matter much that MobX is auto-tracking and hooks are explicit. You just start writing your code in the same way and composing behaviors in the same way, that after a while you ask yourself whether using the library is worth it. Especially when you realize what MobX is doing in relation to React.

React is designed to execute top-down creating a Virtual DOM representation and efficiently diffing and patching the real DOM. MobX inserts itself breaking apart this tree setting up a secondary reactive graph to independently update components that still produce Virtual DOM nodes that still are diffed and patched in the same way. Reactivity is pure overhead over a Virtual DOM if it is already setup to update this way. Nothing is going to beat React’s own update cycle in React.

So it should be no surprise there have been a number of new state solutions that try to do this. Most notably the recent library, Recoil, released by a team at Facebook. It uses state atoms and selectors (lenses) which again are analogous with 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 to see one to understand how MobX’s key advantage has been mostly addressed. Its weakness is now overhead most of the time in the systems coming out of the “new” React. React isn’t done moving along this trajectory either. It should start to be clear the tradeoffs that come with mixing a VDOM with reactivity. Speaking of which…

Vue

Vue is another library that rivals React’s popularity and its reaction to hooks was finally exposing their reactivity system. I say "finally" mostly as a reactivity super fan. From the community standpoint, they have actually moved pretty quickly. But for me right from when I first saw Vue, I always hoped they would. I understood that in the name of easiness they wanted to abstract it away.

Reactivity actually carried negative connotations around the time Vue was gaining popularity. Early reactive UI libraries like KnockoutJS were rapidly falling out of favor due to their reliance on “magic” and their notorious unpredictability.

Vue’s focus on easiness would never let them take this path forward. It’s just something innately in its nature, as it grew into popularity by walking a very tight line between other libraries borrowing the best parts while packaging them up in the most consumable way. Vue by its very nature aims to be a safe library so it is challenging for it to shake the boat.

But now we have the Composition API coming in Vue 3 and it’s a big step forward. This is built on a truly reactive approach using proxies and I’ve truly enjoyed seeing how it has been progressing. It’s still early days for the discovery of the best patterns here. As a newcomer to reactivity, it might be the most approachable library and has all the fundamentals.

It isn’t without some of its own interesting quirks. Like their ref‘s which can be used for simple atoms also act as deep proxies when storing objects. This default definitely ensures things will always update but it also brings heaviness when trying to do simple things. There are helpers like toRef which make the move from the proxy reactive back to ref seamlessly. But this is all to support a consistent API where everything is a proxy.

This has a side effect of a bit of verbosity. Where many libraries use function executions, Vue tags a .value on the end which can’t be renamed or aliased. Still, Vue as a library is learning so I think it is in a good place for people to learn along with 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 are also experimenting with more granular updates which is a good start. However, they still are carrying with them the weight of their Virtual DOM. It’s hard to say if they will change their approach as Vue does tend to like to live in that zone in the middle and this gives them the most flexibility. When I asked Evan You, creator of Vue, this question on Twitter he stated 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 is still a big step forward, as they reduced size by nearly 33% as well as increasing the performance by a similar amount over Vue 2. It’s impressive for a library of this popularity to make improvements of this scale. They’ve even managed to get TypeScript working in their custom template DSL.

Svelte

Another interesting library is Svelte has really come into its own this year. It has taken the coveted 4th slot for UI libraries in most comparisons. It is definitely the library that carries the most interest and that largely comes from its unique approach of being a compiler. Its ability to build animations into its template syntax continues to be a standout feature along with its ability to produce incredibly small bundle sizes.

The code one writes with Svelte is like its own language:

// reactive atom
let count = 0;

// derivation
$: double = count * 2;

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

// update an atom
count++;

You aren’t going to be able to do this with less code than that. Svelte's terseness is impressive. It does have some limitations though.

Without having a proxy or function execution there is no difference except grammar on whether you are dealing with the reactive atom vs the value it contains. We know when assigning we are dealing with the atom. But elsewhere we we can only assume we are reading the value.

It means that everything is localized to the component. Things like stores use a completely different syntax. This in turn actually removes many of the means to write less code as your project scales and can be restrictive.

Svelte strives for the smallest runtime which interestingly means putting more code in each component. This actually has shown to scale worse than most libraries, but not in any really meaningful way.

Svelte might be the best demo library we’ve seen to date. When there is a single component it requires the least code, produces the smallest bundles, and is reasonably performant to boot. Not to say it isn’t good for large applications. But it knows how to drop into most demos and make a mark.

Solid

And now is a good time to talk about Solid. Every library mentioned so far has taken a different tact with reactivity. I want to mention another library with a slightly different take, since I believe it represents the cutting edge of reactive UIs.

Solid has been using things for years that other libraries are only just starting to look into. Things like more granular templates and compiled JSX. It shows that you don't need a VDOM to get the full flexibility of JavaScript.

Solid is influenced by functional programming paradigms like React, uses a compiler like Svelte, but still uses proxies like Vue. However, the biggest difference between it and the others is Solid’s structure isn’t tied to its components but reactive scope itself.

Regardless of how you organize your code and break up your components, at runtime it flattens to a single reactive graph. The compiler just handles the JSX transformation similar to other reactive template DSLs. The obvious benefit there is support for syntax highlighting, tooling, and TypeScript is almost free.

// 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 supports proxies, but there are no special helpers needed as you just wrap with a thunk () => state.count. Most of the time you don’t even bother with derivations as wrapping with a thunk is sufficient. This eliminates a lot of the reactive overhead written and cognitively.

The render system lazily evaluates props meaning that components are ephemeral factory functions whose only purpose is to create closures to wrap over the state. They are essentially gone once executed, much like Vue’s setup function. The state only lives in the reactive computations that depend on it.

What this means is Solid is significantly more performant than the other libraries. Solid’s renderer has been applied to other reactive libraries like Knockout, MobX, and Vue has shown to have similarly impressive performance (ko-jsx, mobx-jsx, and vuerx-jsx). It’s an approach that fully embraces reactivity and makes no attempt to hide it.

Sampling from JS Framework Benchmark — May 2020

Solid isn’t without its quirks. It almost religiously stands on read/write segregation and unidirectional data flow at the cost of ease of use. Even its proxies can’t be updated directly and function as an immutable tree. The setState method produced on creation supports paths similar to ImmutableJS and acts as a lens to update. Although it does have an Immer influenced form for those who can’t live without mutation.

The key is that like React it uses array tuples to avoid naming collisions and allow the ability to read and write be passed separately. At only 2k stars on Github it is definitely an underdog in the overcrowded ecosystem. But its performance and size are impressive and it has a comparable feature set to the other libraries (although fewer than you'd find in Angular or Vue).

Conclusion

Reactivity is still a moving target in 2020, and you have a lot of choices. From avoiding it completely with React, to embracing a renderer built completely on it with Solid, there is a wide range. There is Vue that is starting to embrace its true nature and Svelte who wants you to forget about all this and rediscover where you found joy programming in the first place. This will continue to be a hot topic and a lot of innovation is happening in this space.

Some key developments to pay attention to:

  1. Increased capabilities of Template DSLs. Expect to see better tooling support and TypeScript coming soon. Previously thought impossible JS DSLs like JSX are on the table now, bringing a whole new level of flexibility to this space.
  2. More use of compilation. Svelte’s animation’s comes to mind. But there are many applications. Compilation is the perfect balance to the usually heavier setup cost of reactivity.
  3. Performance and flexibility of granular approaches. Don’t believe this just about doing the most granular updates. Libraries in this space are looking at ways to control reactive boundaries to reduce creation and synchronization costs.

I strongly recommend taking the time to look into at least one of libraries mentioned in this article to familiarize yourself with these patterns. Learning one is enough. In many ways they are all very similar. Without a doubt, we are seeing the biggest fundamental shift in Web UI development in the past 5 years.