Implementing CSS Animations with New Experimental useTransition Hook
Tags: react
Published: 2019-12-19 16:08:10

This is a summarized version of a Japanese article by the same author:


[ワイ「アニメーションするにはこのuseTransitionってのを使えばええんか？」](https://qiita.com/uhyo/items/edd51ca053ed0e7ae83d)

## Introduction

In October 2019, the React team introduced an experimental preview of [React's new Concurrent Mode](https://reactjs.org/docs/concurrent-mode-intro.html). Along with other innovative (but a little bit opinionated) features in it, the new **useTransition** hook has power to completely change how we develop React applications.

Here is a brief introduction of the `useTransition` hook (see the official docs for more details): the `useTransition` hook allows us to maintain two states at the same time, primarily designed for page transitions.

Consider a situation where you update some state in order to transition to a new page. In Concurrent Mode, the rendering of the new state may “suspend” (typically due to data fetching). Without `useTransition`, we have to render fallback contents (often a loading icon) during the suspension. What is important here is that the suspension can only be detected *after* state updates, since suspension occurs as a result of rendering based on the new state.

By utilizing `useTransition`, we can tell React to keep reflecting the old state to the DOM while suspension. As soon as the rendering of the new state completes, React switches the DOM to the new result. Furthermore, `useTransition` provides a flag of whether it is pending (waiting for the rendering of the new state) to the world of the old state. Here React is maintaining two worlds at the same time: one for the old pending state and one for the new state.

You can find nice examples of `useTransition` at [the official documents](https://reactjs.org/docs/concurrent-mode-patterns.html#transitions).

## Using useTransition for Animations

As is obvious from its name, the `useTransition` hook is fairly useful for implementing CSS animations based on the `transition` CSS property. This article shows how to use `useTransiton` for this purpose and gives a brief explanation.

The well-known problem regarding CSS animations in React is how to handle mounting and unmounting of components. If we want to utilize CSS transitions, we cannot mount a new DOM element and start its animation in one action; we have to mount an element in the before-animation state first, and then immediately alter its style to after-animation one to trigger the animation.

Previously, people used libraries like [react-transition-group](https://github.com/reactjs/react-transition-group) or [react-spring](https://www.react-spring.io/) to handle this situation. These libraries automates the above two-step state changes.

In this article, an alternative approach which utilizes `useTransition` is introduced. Below is an example though it is still a rough PoC:

{% codesandbox qiita-wai-react-2-uj246 %}

In the example, you can click the `toggle` button to show and hide a blue box. The box animates its `opacity` on every state change and is actually mounted/unmounted every time. As usual, unmounting is delayed until the animation completes. In what follows, the trick used in this example is explained.

## Preparation

We start from looking at some utilities defined in the example. The first is the `Timer` class:

```js
// src/Timer.js
export class Timer {
  constructor(duration) {
    const timer = new Promise(resolve => setTimeout(resolve, duration));
    this.done = false;
    this.promise = timer.then(() => {
      this.done = true;
    });
  }
  throwIfNotDone() {
    if (!this.done) {
      throw this.promise;
    }
  }
}
```

`new Timer(duration)` creates a Promise which is fulfilled after `duration` milliseconds. The `throwIfNotDone` method, when called, throws that Promise if it is not fulfilled yet. We don't step into details, but throwing Promises is a significant characteristic of React's Concurrent Mode. In short, throwing a Promise means that the current rendering should be suspended until that Promise is fulfilled.

So we need a component that actually throws this Promise. It's called `Waiter` in the example. It can't be simpler; it receives a Timer from props and calls its `throwIfNotDone` method. It does not produce actual contents.

```js
function Waiter({ timer }) {
  if (timer) timer.throwIfNotDone();
  return null;
}
```

React has a rule that, if a component may throw a Promise it must be enclosed in React's `Suspense` component. That's why `Waiter` is used as follows in the example.

```jsx
      <Suspense fallback={null}>
        <Waiter timer={timer} />
      </Suspense>
```

Thanks to `Timer` and `Waiter`, we now have ability to cause suspensions for a certain period of time while rendering. In the example we prepare the `timer` state which is passed to `Waiter`. If you create a `Timer` and set the `timer` state to it, the next rendering would be suspended for the specified time.

## Two-Step Rendering Using useTransition

Now, let's see how the animation is implemented in the example. First of all, the blue box is rendered by the following code:

```jsx
      {show ? <Box show={show && !isPending} /> : null}
```

where `show` is a boolean state and `isPending` is a flag provided by `useTransition` indicating whether some rendering is suspended now. Normally `isPending` is `false` and it becomes `true` only while suspension.

The `Box` component renders a blue box; if `show={false}` its `opacity` is `0` and if `show={true}` its `opacity` is `1`. It is worth noting that the `Box` component is actually unmounted while `show` is `false`.

Finally we see what happens when we click the `toggle` button:

```js
  const toggle = () => {
    if (show) {
      startTransition(() => {
        setShow(false);
        setTimer(new Timer(500));
      });
    } else {
      setShow(true);
      startTransition(() => {
        setTimer(new Timer(10));
      });
    }
  };
```

If `show` is `false`, we call `setShow(true)`, which will update `state` to `true`. The point is the `startTransition` call following it; it takes a callback function which is immediately called. The `useTransition` hook works for the states updated inside the callback; if these state updates caused a suspension, then React renders the old state while setting `true` to `isPending` during the suspension.

![Illustration](https://thepracticaldev.s3.amazonaws.com/i/2la945mm8ucj1566vzxr.jpeg)

Here is an illustration of what happens here. In the initial state (the left box in the above image) both `show` and `isPending` are false. When `toggle()` is called, `show` is set to `true` as usual. Also, `timer` is set to `new Timer(10)` inside `startTransition`. As explained above, this will trigger a suspension which leads to the middle state (where `show` is `true` and `isPending` is `true`) being rendered to the DOM. After 10ms the suspension finishes and the last state (`show` is `true` and `isPending` is `false`) is rendered.

Here we achieved the two-step rendering with one set of state updates by cleverly utilizing suspension and `isPending` provided by `useTransition`.

---

Next we see what happens when `trigger()` is called while `show` is `true`.

```js
      startTransition(() => {
        setShow(false);
        setTimer(new Timer(500));
      });
```

![Illustration](https://thepracticaldev.s3.amazonaws.com/i/3gokx0evfob3xt52anwq.jpeg)

In the initial state `show` is `true` and `isPending` is `false`. Basically we are doing the same: set `show` to `false` and set a `Timer`. This time the duration of timer is 500 ms; this is the duration of the animation of `opacity`.

The point is that, this time the update for `show` is also put inside `startTransition`. Thanks to this, the value of `show` keeps the old one while the suspension caused by `timer`. This is why in the middle state `show` is still `true` while `isPending` is updated to `true`. After 500 ms it transitions to the last state where `show` is updated to `false` and `isPending` is set back to `false`.

## Conclusion

This article explained how to utilize `useTransition` from React's Concurrent Mode to implement CSS animations. Thanks to the `isPending` flag provided by it, we can pack a two-step rendering into one set of state updates.