React: Fine-grained reactivity should be the norm
Tags: frontend, webdev, react, javascript
Published: 2023-09-15 13:48:43

This article attempts to explain why the current state of React has fundamental flaws so we can slowly build the knowledge to understand how to overcome them, for both better user and developer experience.

**Disclaimer:** We will need to simplify some concepts a little bit for didactic reasons, but you'll find a list with a lot of great content in the references section, in case you're curious. Also, you should have some familiarity with React, its Virtual Dom, and basic hooks.

## Table of Contents
- [If you wanna try yourself](#if-you-wanna-try-yourself)
- [React rendering model is flawed](#react-rendering-model-is-flawed)
- [Introducing Fine-grained reactivity 🤌](#introducing-finegrained-reactivity)
  * [First: Observer Pattern! 👀](#first-observer-pattern)
  * [Coming up with a better definition](#coming-up-with-a-better-definition)
- [The easiest way to achieve fine-grained reactivity in React](#the-easiest-way-to-achieve-finegrained-reactivity-in-react)
- [Conclusion](#conclusion)
- [References](#references)

## If you wanna try yourself

First, make sure you have [React developer tools](https://react.dev/learn/react-developer-tools) installed in your browser and check the [Highlight updates when components render](https://www.youtube.com/watch?v=R2HULHV8R8M) option.

Second, [here is the repo with all examples](https://github.com/gabrielprrd/fine-grained-reactivity-example). You can simply clone it and follow the instructions on the readme.

We'll be using an application created with `create-next-app` configured with Typescript and TailwindCSS.

Now, let's go!

## React rendering model is flawed

Don't get me wrong: React is amazing, robust and has been giving life to all sorts of projects for over a decade. But that's precisely why people are able to detect its flaws and are so passionate about improving it (or coming up with a new competitor once in a while).

So, the problem we are going to discuss today is **rendering**. React rendering model basically makes a whole component and its children re-render on every state change. Let's take a look at a very basic example:

```tsx
import { useState } from "react";
import Button from "@/components/Button";

export default function DefaultCounter() {
  const [count, setCount] = useState(0);

  return (
    <div className="bg-green-900 p-2 rounded-md">
      <h2 className="text-xl text-white">Default counter</h2>
      <p className="text-white">Counter: {count}</p>
      <Button onClick={() => setCount(count + 1)}>Increase</Button>
    </div>
  );
}
``` 
Every time we click on the button, the count increases (the state changes), which causes a re-render in every child of our `DefaultCounter` component. Notice the highlight in our `Button`.


![Cursor clicking on the button, incrementing the counter and making everything re-render](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/19lqkjspv945xzidf4sp.gif)

Here is another example, passing props to a child counter display and an unrelated component. Passing props or not is irrelevant in this case.

```tsx
import { useState } from "react";
import UnrelatedElement from "@/components/UnrelatedElement";
import Button from "@/components/Button";

export default function CounterPassingProp() {
  const [count, setCount] = useState(0);

  return (
    <div className="bg-green-900 p-2 rounded-md">
      <h2 className="text-xl text-white">Outer Counter passing prop</h2>
      <p className="text-white">Counter: {count}</p>
      <Button onClick={() => setCount(count + 1)}>Increase</Button>

      <InnerCounter count={count} />
      <UnrelatedElement />
    </div>
  );
}

type Props = {
  count: number;
};

export function InnerCounter({ count }: Props) {
  return (
    <div className="bg-green-600 p-2 rounded-md my-2">
      <h2 className="text-xl text-white">Inner counter receiving prop</h2>
      <p className="text-white">Counter: {count}</p>
    </div>
  );
}
```
Again, everything is re-rendering.

![Cursor clicking on the button, incrementing the counter and making everything re-render](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6lmi23y17q8hng4qb1xn.gif)

We could maybe try to substitute the counter state for a ref, since state is the cause of re-rendering, right?
```tsx
export default function CounterWithRef() {
  const count = useRef(0);

  return (
    <div className="bg-green-900 p-2 rounded-md">
      <h2 className="text-xl text-white">Counter using ref</h2>
      <p className="text-white">Counter: {count.current}</p>
      <Button onClick={() => count.current++}>Increase</Button>
    </div>
  );
}
```

![Cursor clicking on the button, but nothing happens](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3ol1rl5ogwtbyf6gkpnc.gif)

Well... nothing seems to happen because ref doesn't force **any** part of the component to re-render, so we can't see the updated value of the counter on the screen.

Could we use some hack like directly modifying `innerHTML`? Maybe, but that's totally an anti-pattern. A way to force a re-render would be creating a new state like:

```tsx
const [, setForceUpdate] = useState(Date.now());
```

And then calling `setForceUpdate` when the click event is triggered, which also completely kills our purpose and causes a lot of confusion. Please, don't do it!

Those performance problems are only partially fixable and the cost in complexity for something like that doesn't make much sense. In our example, specifically, the best we could do is to wrap the elements that don't have props changing with `memo`:

```tsx
const MemoUnrelatedElement = memo(UnrelatedElement);

// and then we use inside our Counter component
<MemoUnrelatedElement />
```

From the [docs](https://react.dev/reference/react/memo): 

> `memo` lets you skip re-rendering a component when its props are unchanged.

Now UnrelatedElement won't re-render. Nice!

But what about the other children?
- Wrapping InnerCounter with `memo`, would be irrelevant since it receives the count state as a prop, which is changing.
- Wrapping Button with `memo` would also be irrelevant. Button receives the setter function as a prop, which has changed.

What about abstracting the setter passed to Button and wrapping it with the useCallback hook?

```tsx
  const increment = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  // and then
  <Button onClick={increment}>increment</Button> 
```

It won't work because it was not designed for it. The default behavior of React is to recreate `increment` declaration in every render, pointing to a new reference (remember: functions are not primitive values, so "same implementation" doesn't mean "same function").

useCallback was created to cache that declaration, making sure we are pointing to the same reference, **unless some dependency changes**, which is exactly our case.

This is the best we've got so far:

![Cursor clicking on the button, incrementing the counter and making everything re-render, except UnrelatedElement](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pi66ptwuzvrg645qku1g.gif)

There must exist a better solution, and luckily, it does!

## Introducing Fine-grained reactivity 🤌

Trying to come up with a definition, I've found it quite difficult to express it formally. A very vague definition would be "an architectural approach that focuses on efficiently updating and rendering user interfaces in response to changes in the underlying data or state."

But that doesn't say anything at all.

### First: Observer Pattern! 👀

In order to learn how fine-grained reactivity is implemented, we need to understand the concept of Observers.

Simplified as much as possible, this design pattern contains two types of entities:
- Subject: an entity that contains a list of observers for each type of event
- Observers: entities subscribed to the Subject that get notified by it based on a type of event

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y8reyf8wacgb15rty2zb.png)

Think of the Subject as a job portal that sends you notifications with open positions based on your profession.
```ts
subject.notify("software-dev");
```
You, as an Observer subscribed on the "software-dev" list, will do something with that information, like opening the link to the open position's page.

```ts
observer.update();
```

For real-world examples with detailed implementation, I highly recommend reading [this explanation](https://refactoring.guru/design-patterns/observer), but the idea of notifying a list of observers based on a type of event is enough to get the following topics.

### Coming up with a better definition
Fine-grained reactivity aims for performance. The goal is to track changes and re-render the UI as least as possible, through a graph full of nodes. Those nodes here are conceptually called Primitives. Let's take a look at each one of them.

- **Signals**: observables (event emitters), which we can access their values through getters and setters.
- **Reactions**: observe our Signals and re-run them every time the Signal's value changes.
- **Derivations**: memoization mechanisms that cache expensive derived values.

So you can see this technique as turning the states into observables in a graph that notifies changes in any children.

Those concepts are widely implemented in other frameworks like Qwik, Solid.js, and Preact. Each one approaches reactivity with different mechanisms that take care of unused nodes, clean-ups, keeping the graph as shallow as possible, ensuring synchronous execution through transactions, etc. I won't dare to touch those subjects for now.

## The easiest way to achieve fine-grained reactivity in React 🔥

Disclaimer: that's solely my point of view.

[Legend State](https://legendapp.com/open-source/) is a state-management library that achieves that level of efficiency. It exposes an API with the primitives discussed above through custom hooks and wrappers for our components.

Let's rebuild our simple counter:

```tsx
import { ObservablePrimitiveBaseFns } from "@legendapp/state";
import { useObservable, Memo } from "@legendapp/state/react";
import UnrelatedElement from "@/components/UnrelatedElement";
import Button from "@/components/Button";

export default function FineGrainedCounter() {
  const count$ = useObservable(0);

  return (
    <div className="bg-green-900 p-2 rounded-md">
      <h2 className="text-xl text-white">Legend Counter</h2>
      <p className="text-white">
        Counter: <Memo>{count$}</Memo>
      </p>
      <Button onClick={() => count$.set((prev) => prev + 1)}>increment</Button>

      <InnerCounter count$={count$} />
      <UnrelatedElement />
    </div>
  );
}

type Props = {
  count$: ObservablePrimitiveBaseFns<number>;
};

export function InnerCounter({ count$ }: Props) {
  return (
    <div className="bg-green-600 p-2 rounded-md my-2">
      <h2 className="text-xl text-white">Inner counter receiving prop</h2>
      <p className="text-white">
        Counter: <Memo>{count$}</Memo>
      </p>
    </div>
  );
}
```

`useObservable` is a custom hook that creates a Signal count$ with `get`, `set` and `value`. In this case, our previous count state. The $ (dollar sign) at the end of the variable is a convention.

![Cursor clicking on the button, incrementing the counter but nothing re-renders](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yhj6749rxgad16oxh6zd.gif)

Mind-blowing 🤯

Also, let's get an example with derivations. Here is probably the most used example in the history of computed values:

```tsx
import { useState } from "react";

export default function DefaultFullName() {
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");

  return (
    <div className="bg-green-900 p-2 gap-2 flex flex-col rounded-md w-[30ch]">
      <h2 className="text-xl text-white">Default computed</h2>

      <FirstName handleChange={(e) => setFirstName(e.target.value)} />
      <LastName handleChange={(e) => setLastName(e.target.value)} />

      <p className="text-white">
        Full name: {firstName} {lastName}
      </p>
    </div>
  );
}

function FirstName({ handleChange }) {
  return (
    <>
      <label htmlFor="lastName" className="text-white">
        Last name:
      </label>
      <input id="lastName" onChange={handleChange} />
    </>
  );
}

function LastName({ handleChange }) {
  return (
    <>
      <label htmlFor="firstName" className="text-white">
        First name:
      </label>
      <input id="firstName" onChange={handleChange} />
    </>
  );
}
```

As we saw before, everything is going to re-render on every state change:

![Filling a form with two fields and all elements re-render on every key press](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/41qtpeabrs9nabg6i41l.gif)

Now, the same component with Legend State:

```tsx
import { useObservable, observer, Memo } from "@legendapp/state/react";
import { ObservableObject } from "@legendapp/state";

const FullName = ({
  user,
}: {
  user: ObservableObject<{
    firstName: string;
    lastName: string;
  }>;
}) => (
  <p className="text-white">
    Full name: <Memo>{user.firstName}</Memo> <Memo>{user.lastName}</Memo>
  </p>
);

const FirstName = observer(
  ({
    user,
  }: {
    user: ObservableObject<{
      firstName: string;
      lastName: string;
    }>;
  }) => (
    <div className="flex flex-col gap-2">
      <label htmlFor="firstName" className="text-white">
        First Name
      </label>
      <input
        id="firstName"
        value={user.firstName.get()}
        onChange={(e) => user.firstName.set(e.target.value)}
      />
    </div>
  )
);

const LastName = observer(
  ({
    user,
  }: {
    user: ObservableObject<{
      firstName: string;
      lastName: string;
    }>;
  }) => (
    <div className="flex flex-col gap-2">
      <label htmlFor="lastName" className="text-white">
        Last Name
      </label>
      <input
        id="lastName"
        value={user.lastName.get()}
        onChange={(e) => user.lastName.set(e.target.value)}
      />
    </div>
  )
);

export default function FineGrainedFullName() {
  const user = useObservable({
    firstName: "",
    lastName: "",
  });
  return (
    <div className="bg-green-900 p-2 gap-2 flex flex-col rounded-md w-[30ch]">
      <h2 className="text-xl text-white">Fine Grained Full Name</h2>
      <FirstName user={user} />
      <LastName user={user} />
      <FullName user={user} />
    </div>
  );
}
```

![Filling a form with two fields and only the active one re-renders on every key press](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5wtrvbf14f2xirlwyzih.gif)

Note how the renders are isolated to the input components changing their state - in this case, their signal's value. And how the full name field also doesn't trigger a re-render even dynamically being updated!

Legend does way more than that: It simplifies context, global state, persistency, reference values, and much more, but we'll keep that for a future article.

## Conclusion

That's it, devs! If you wanna deep-dive on the subject, I can't recommend the links on the references section enough. 🤓

Thank you so much for reading.

## References
[https://refactoring.guru/design-patterns/observer/typescript/example](https://refactoring.guru/design-patterns/observer/typescript/example)
[https://indepth.dev/posts/1269/finding-fine-grained-reactive-programming#how-it-works](https://indepth.dev/posts/1269/finding-fine-grained-reactive-programming#how-it-works)
[https://legendapp.com/open-source/legend-state/](https://legendapp.com/open-source/legend-state/)
[https://legendapp.com/open-source/state/fine-grained-reactivity/](https://legendapp.com/open-source/state/fine-grained-reactivity/)
[https://www.builder.io/blog/usesignal-is-the-future-of-web-frameworks](https://www.builder.io/blog/usesignal-is-the-future-of-web-frameworks)
[https://dev.to/ryansolid/a-hands-on-introduction-to-fine-grained-reactivity-3ndf](https://dev.to/ryansolid/a-hands-on-introduction-to-fine-grained-reactivity-3ndf)
[https://medium.com/hackernoon/becoming-fully-reactive-an-in-depth-explanation-of-mobservable-55995262a254](https://medium.com/hackernoon/becoming-fully-reactive-an-in-depth-explanation-of-mobservable-55995262a254)
[https://preactjs.com/blog/introducing-signals/](https://preactjs.com/blog/introducing-signals/)
[https://hygraph.com/blog/react-memo](https://hygraph.com/blog/react-memo)