Adrien Gautier
Adrien Gautier

Adrien Gautier

How React's approach to reactivity can be challenged?

Adrien Gautier's photo
Adrien Gautier
·Jan 6, 2022·

6 min read

As a React developer, I never really questioned React's approach to reactivity. But I recently discovered two other frameworks: SolidJS and Svelte. Both frameworks have in common not to rely on a virtual DOM. But they are also interesting because they offer two radically different implementations of what I will call, "opt-in reactivity".

To understand what I mean by "opt-in reactivity", I must first explain how I think React follows an "opt-out reactivity" approach.

React's "opt-out reactivity"

To opt-out in my mind means to explicitly/actively unsubscribe from something (e.g. to opt-out from a mailing list). Opt-out also suggests that if we don't do anything we will be subscribed to this thing.

Opt-out (from) reactivity, thus means, explicitly making something non-reactive or less reactive.

So how does it apply to React?

In a React functional component, any statement in the function is by default reactive.

function MyComponent(props) {
    // executed each time a prop changes
}

That means it will be executed if any props or states change.

To write a not-always-reactive statement, we can rely on the useEffect function.

function MyComponent(props) {
    useEffect(() => {
        // executed only if the "name" prop changes
    }, [props.name])
}

The same logic applies for useMemo and useCallback which allow recomputing a variable value only if one of the dependencies changes.

Think about this: Most of the time we fight to make a component re-render less.

This approach, while powerful in most situations (especially compared to Class Component lifecycle), can be challenged and has been challenged by other frameworks like SolidJS or Svelte.

Opt-in reactivity

As opposed to opt-out, "opt-in reactivity" means non-reactive unless specified.

As I said, SolidJS and Svelte have two radically different implementations of the same approach.

Svelte

In order to understand Svelte approach, I need to make a quick overview of Svelte's custom component format.

Introduction to Svelte components

Components are written into .svelte files, a superset of HTML. These files contains three (all optional) sections:

<script>
    // logic goes here
</script>

<!-- markup (zero or more items) goes here -->

<style>
    /* styles go here */
</style>

The markup section relies on a template syntax using curly braces {expression} for JavaScript expressions and blocks like {#if ...} ... {/if} for conditional, loop, await logic. This section can access variables declared or imported (i.e. stores) in the <script> section.

Props can be declared using the export keyword on a local variable.

<script>
    export let foo;
</script>

This approach as you can see is quite different from React's (or SolidJS') functional approach. It can be confusing at first because it transforms/add a new meaning to existing JavaScript concepts but is very powerful because removes tons of boilerplate in the end.

What is reactive by default?

By default in Svelte, everything in the <script> tag is executed once.

<script>
    export let name;

    console.log(name); // executed once
</script>
<h1>
    Hello {name} ! <!-- updated every time the prop changes -->
</h1>

But every variable specified in the markup will react to a value change.

Adding an internal state

Because assignments are reactive, updating a local variable will trigger a re-render and act as an internal state.

<script>
    import { onDestroy } from 'svelte';
    let elapsedSeconds = 0; // declaring a local variable

    function addSecond() {
        elapsedSeconds += 1; // triggers a re-render
    }

    const interval = setInterval(addSecond, 1000);

    onDestroy(() => {
        clearInterval(interval);
    });
</script>

<h1>Elapsed seconds: {elapsedSeconds}</h1> <!-- re-rendered every second -->

Adding reactive statements

In some situation, we need to trigger side-effects when a state changes. In Svelte, this works by prefixing the statement with $: JS label:

<script>
    let elapsedSeconds = 0;

    function addSecond() {
        elapsedSeconds += 1;
    }

    setInterval(addSecond, 1000);

    $: console.log(elapsedSeconds); // called every second
</script>

This not only works to trigger side-effects but also works to compute "derived" variables:

<script>
    export let name;

    $: sentence = `Hello ${name}!`; // updated every time the name changes
</script>
<h1>{sentence}</h1>

Of course, there is a lot more to cover about Svelte, but here was a quick look at Svelte "opt-in" reactivity.

SolidJS

As I said, SolidJS's "opt-in" reactivity is quite different from Svelte. In fact, its implementation is closer to React's API.

What is reactive by default?

Because SolidJS rely on JSX, the following code seems really familiar coming from React:

function Heading(props) {
    console.log(props.name); // executed once

    return (<h1>Hello {props.name} !</h1>); // updated every time the name changes
}

This can be misleading because the console.log statement would be called every time a prop changes in React but not in SolidJS. Similar to Svelte, only the returned markup is reactive to a prop change.

Adding an internal state

Adding an internal state in SolidJS is also familiar. Internal state in SolidJS is called a signal (inspired by S.js) and is created using an API similar to the useState hook in React:

function Counter() {
  const [elapsedSeconds, addElapsedSeconds] = createSignal(0);

  const addSecond = () => addElapsedSeconds(elapsedSeconds() + 1);

  const interval = setInterval(addSecond, 1000);

  onCleanup(() => {
      clearInterval(interval);
  })

  return (<h1>Elapsed seconds: {elapsedSeconds()}</h1>);
}

Note: createSignal returns a pair of functions: a getter and a setter.

Adding reactive statements

Creating reactive side-effects is similar to the useEffect function in React.

function Counter() {
  const [elapsedSeconds, addElapsedSeconds] = createSignal(0);

  const addSecond = () => addElapsedSeconds(elapsedSeconds() + 1);

  setInterval(addSecond, 1000);

  createEffect(() => {console.log(elapsedSeconds())}); // called every second
}

The difference is that any signal specified in the scope of the given function will trigger the side-effect if its value is updated.

It is however possible to make reactive dependencies explicit using the on helper.

Creating a derived state works by using the createMemo helper which recalls the useMemo hook in React.

function Heading(props) {
    const sentence = createMemo(() => `Hello ${props.name()}!`);

    return (<h1>{sentence()}</h1>); // updated every time the name changes
}

With this quick overview, you can see how SolidJS is very similar to React when it comes to its API. This can be misleading if we are not fully aware of the difference between the "opt-out" and "opt-in" reactivity strategy.

SolidJS can be confusing in other aspects, like how props reactivity is handled. Some statements, quite common in React, will not work:

// bad
const BasicComponent = (props) => {
 const { value: valueProp } = props;
 const value = createMemo(() => valueProp || "default");
 return <div>{value()}</div>;
};

// bad
const BasicComponent = (props) => {
 const valueProp = props.value;
 const value = createMemo(() => valueProp || "default");
 return <div>{value()}</div>;
};

Conclusion

"Opt-in" reactivity, as I call it, is the cornerstone of fine-grained reactivity. The pattern Signal / Reaction / Derivation, allows frameworks like Svelte and SolidJS to optimize performances by managing only the needed subscriptions.

I believe this approach also allows these frameworks to operate without any Virtual DOM which makes a fundamental difference in performances.

An evidence of this paradigm shift is the tweened/spring helpers in Svelte. These helpers produce values that can change every time the screen refreshes (generally 60 times a second). That means every subscriber (e.g. DOM node, derived value) is also updated at the same pace!

This can also be done in SolidJS. Here is an example where I update a signal for every animation frame.

This cannot really be achieved in React. Third-party libraries like react-spring use custom animated variables with custom DOM components.