React 18 - performance improvements
Tags: javascript, react
Published: 2022-05-29 21:14:14

Recently React released version 18 with some great features.
In this post we will take closer look at performance related features.
<br />

## useTransition

It's part of the concurrency concept where you can prioritize state updates.
Urgent state updates can be prioritized over less urgent (blocking) updates.

How to use it and how this new hook improves your app performance will learn in the example which can be found [here](https://gitlab.com/markoarsenal/react-18-concurrency).

This is our example. It's a simple one, we have a button that opens a modal, and within the modal, we render a list of 500 comments.
500 comments is a lot but this will work just fine on most devices.

```javascript
import { useState } from "react";
import Comments from "../components/Comments";
import Modal from "../components/Modal";
import data from "../data/index.json";

export default function Home() {
  const [isOpen, setIsOpen] = useState(false);
  const [comments, setComments] = useState([]);

  return (
    <div className="p-4">
      <button
        className="px-4 py-2 border-none rounded-sm bg-blue-800 text-white"
        onClick={() => {
          setIsOpen(true);
          setComments(data);
        }}
      >
        Toggle modal
      </button>
      <Modal
        isOpen={isOpen}
        onClose={() => {
          setIsOpen(false);
          setComments([]);
        }}
      >
        <Comments comments={comments} />
      </Modal>
    </div>
  );
}
```

But, if we slow down the rendering of the `Comment` component 😈 things get more interesting.
To achieve that I added `for` loop to iterate one milion times.

```javascript
const Comment = ({ name, email, body, className, onClick }: CommentProps) => {
  const soooSloww = [];

  for (let i = 0; i < 1000000; i++) {
    soooSloww.push(i);
  }

  return (
    <article className={className} onClick={onClick}>
      <h3 className="font-semibold">{name}</h3>
      <h4 className="text-gray-500 italic">{email}</h4>
      <p>{body}</p>
    </article>
  );
};
```

**Now, when you click the button to open the modal nothing happens for a few seconds.**
That's because the browser is busy rendering slow 500 `Comment` components.
After some time the modal and the comments are rendered.

From a user perspective, this is very bad UX.
**How to improve it?**
We can prioritize renders, and in our example, it's more important to first render the modal and after that comments.

`useTransition` hook returns two variables, `pending` which is a boolean flag that the transition is not yet finished, and `startTransition` function where you execute your less important state updates.

```javascript
const [pending, startTransition] = useTransition();
```

Now, our example look like this

``` javascript
export default function Home() {
  const [isOpen, setIsOpen] = useState(false);
  const [comments, setComments] = useState([]);
  const [pending, startTransition] = useTransition();

  return (
    <div className="p-4">
      <button
        className="px-4 py-2 border-none rounded-sm bg-blue-800 text-white"
        onClick={() => {
          setIsOpen(true);
          startTransition(() => {
            setComments(data);
          });
        }}
      >
        Toggle modal
      </button>
      <Modal
        isOpen={isOpen}
        onClose={() => {
          setIsOpen(false);
          setComments([]);
        }}
      >
        {pending ? "Loading..." : <Comments comments={comments} />}
      </Modal>
    </div>
  );
}
```

You can notice that on the button click we update the state to show the modal, which is the action with the higher priority, and update the comments state within the `startTransition` function which tells React that state update is with lower priority.

Also, we used the `pending` flag to show a user the' Loading...' text while slow comments are rendered.
Now, after clicking the button you will immediately get the modal which looks like this:

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

Much better user experience! 😀
<br />

## useDeferredValue

This hook also tells React that certain state updates have a lower priority.
It is similar to the `useTransition` and to be honest I'm not sure what are use cases when you should prefer `useDeferredValue` over `useTransition`, if you have an idea please let me know in the comments. 👇

Our previous example now looks like this and behaves similarly except we don't have the `pending` flag.

```javascript
export default function UseDeferredValues() {
  const [isOpen, setIsOpen] = useState(false);
  const [comments, setComments] = useState([]);
  const commentsToRender = useDeferredValue(comments);

  return (
    <div className="p-4">
      <button
        className="px-4 py-2 border-none rounded-sm bg-blue-800 text-white"
        onClick={() => {
          setIsOpen(true);
          setComments(data);
        }}
      >
        Toggle modal
      </button>
      <Modal
        isOpen={isOpen}
        onClose={() => {
          setIsOpen(false);
          setComments([]);
        }}
      >
        <Comments comments={commentsToRender} />
      </Modal>
    </div>
  );
}
```

<br />
<br />

## Automatic batching

When you are working with React you should aim to have rerenders as less as possible.
Now React 18 helps you to achieve that with **automatic batching**.

Earlier versions of React batched multiple state updates only inside React event handlers like `onClick` or `onChange` to avoid multiple re-renders and improve performance.

Now, React batches state updates in React events handlers, promises, setTimeout, native event handlers and so on.

```javascript
const AutomaticBatching = () => {
  const [countOne, setCountOne] = useState(0);
  const [countTwo, setCountTwo] = useState(0);

  console.log("render");

  const onClick = useCallback(() => {
    setCountOne(countOne + 1);
    setCountTwo(countTwo + 1);
  }, [countOne, countTwo]);

  useEffect(() => {
    document.getElementById("native-event").addEventListener("click", onClick);

    return () =>
      document
        .getElementById("native-event")
        .removeEventListener("click", onClick);
  }, [onClick]);

  const onClickAsync = () => {
    fetch("https://jsonplaceholder.typicode.com/todos/1").then(() => {
      setCountOne(countOne + 1);
      setCountTwo(countTwo + 1);
    });
  };

  const onClickTimeout = () =>
    setTimeout(() => {
      setCountOne(countOne + 1);
      setCountTwo(countTwo + 1);
    }, 200);

  return (
    <div className="p-4">
      <ul className="mb-8">
        <li>Count one: {countOne}</li>
        <li>Count two: {countTwo}</li>
      </ul>
      <Button onClick={onClick}>Batching in click event</Button>
      <Button id="native-event" className="ml-4">
        Batching in native click event
      </Button>
      <Button className="ml-4" onClick={onClickAsync}>
        Batching in fetch
      </Button>
      <Button className="ml-4" onClick={onClickTimeout}>
        Batching in timeout
      </Button>
    </div>
  );
};
```
In this example, you can see that in every event handler we have two state changes but only **one rerender**. You can notice that with one console.log for each event.
<br />

## Improved Suspense

`Suspense` works with `React.lazy` in that way that suspends component rendering until it's loaded and during that time renders a fallback.

```javascript
const LazyComponent = lazy(() => import("../components/LazyComponent"));

<Suspense fallback={<div>Loading...</div>}>
  <LazyComponent />
</Suspense>
```
This is a great way to improve performance in that way you will not include in the initial bundle some parts of the app that you don't need immediately (e.g. modals).

But, `Suspense` is not a new feature, it existed in the previous versions of React, the new is that now it **works with server-side rendering** which wasn't the case before.

That's all folks, hope you like the new version of React. 😀
All examples from above can find it [here](https://gitlab.com/markoarsenal/react-18-concurrency).