React Animations

Previously, I wrote about React animations for creating a dropdown menu, but after spending more and more time working with them, I decided to explore exactly how animations fit into the React lifecycle.

Two Animations

There are two kinds of component animations in React.

  1. The component is already in the DOM
  2. The component is entering the DOM

The first kind are not that hard to deal with. In the past, I’ve added a CSS class to trigger a transition. There are also React specific libraries like radium.

The second kind of component requires a bit more work.

To understand why, consider the React lifecycle.

React Lifecycle

In React, you create a series of components that become part of a the virtual DOM:

1
2
3
4
5
6
7
8
9
10
11
12
13
"PotatoHead": {
    "head": {
        "peg": null
    },
    "body": {
        "topPeg": "eyes",
        "middlePeg": null,
        "bottomPeg": null
    },
    "bottom": {
        "peg": "shoes"
    }
}

The virtual DOM renders into the DOM as seen in the browser:

Eventually something happens in the app and that triggers an action:

1
dispatch(existentialCrisis());

This changes the virtual DOM:

1
2
3
4
5
6
7
8
9
10
11
12
13
"PotatoHead": {
  "head": {
    "peg": null
  },
  "body": {
    "topPeg": "eyes",
    "middlePeg": null,
    "bottomPeg": "mouth"
  },
  "bottom": {
    "peg": "shoes"
  }
}

Which updates the actual DOM:

The thing that makes React animations difficult is that there is no point in the lifecycle that they fit in.

Think about the lifecycle hooks:

componentWillMount
render
componentDidMount

Where are animations supposed to fit in?

You can’t use the componentWillMount hook because there is no DOM element to animate.

And you can’t use the componentDidMount hook because the element is already there, so you would have to rerender it (causing a potential loop) and you may see a jump as the component gets an orientation change after being added to the DOM.

Solution: More Lifecycle Hooks

The React team recognized this problem and created a higher level component called ReactTransitionGroups that can wrap additional components giving them more lifecycle hooks.

componentWillMount render componentDidMount

Any component that is wrapped within a ReactTransitionGroup component will get a couple new lifecycle hooks. One of the most relevant is componentWillEnter which will be fired as soon as the component is mounted (the same time as componentDidMount).

ReactTransitionGroup ReactTransitionGroup componentWillMount componentDidMount componentWillEnter render

componentWillEnter creates a lifecycle hook that we can use to animate an components we want. All other animations will be blocked until a callback is called.

Chang Wang has an excellent example of how to build an animation with Tween using ReactTransitionGroups. If you need fine grained control or want to use a specific library, than ReactTransitionGroups are the way to go.

If you need something even easier, React made a further abstraction called ReactCSSTransitionGroups that utilize transition groups but allow the developer to use CSS transitions to handle any animations.

ReactCSSTransitionGroups

ReactCSSTransitionGroups work by wrapping components and then adding specific classes to child components for a designated amount of time.

Here’s an example of how you would set it up:

1
2
3
4
5
6
7
8
9
10
11
const ShapeContainer = ({elements}) => (
  <div id = "shapes">
    <ReactCSSTransitionGroup
      transitionName = "shape"
      transitionEnterTimeout={2000}
      transitionLeaveTimeout={2000}
     >
      {elements}
    </ReactCSSTransitionGroup>
  </div>
)

Notice a few things. I gave the transition name of shape and a transition enter timeout of 2000 milliseconds and the same for the transition leave.

This means that a few classes with the base name shape will be added to every child component for 2 seconds before they are automatically removed.

To take advantage of any transitions, we need to define them with CSS:

1
2
3
4
5
6
7
8
.shape-enter {
  transform: scale(0);
}

.shape-enter.shape-enter-active {
  transform: scale(1);
  transition: all 2s ease-in;
}

Rendered code will look like this. The span tag is the ReactCSSTransitionGroup (although you can specify other tags like div or ul).

1
2
3
4
<div id="shapes">
  <span data-reactid="0.1">
  </span>
</div>

Any child componenent that is added will receive that shape-enter class. This sets up the initial styling that will be animated (in this example it is effectively hidden).

1
2
3
4
5
6
7
8
<div id="shapes">
  <span data-reactid="0.1">
      <svg data-reactid=".0.2"
           class="shape-enter">
        <circle data-reactid=".0.2"></circle>
      </svg>
  </span>
</div>

In the next tick, the component will receive the shape-enter-active class which will trigger the CSS transition. In this example, it will scale it up to full size. The timing for the transition should match the timeout on CSS transition group.

1
2
3
4
5
6
7
8
<div id="shapes">
  <span data-reactid="0.1">
      <svg data-reactid=".0.2"
           class="shape-enter shape-enter-active">
        <circle data-reactid=".0.2"></circle>
      </svg>
  </span>
</div>

After the timeout is reached, the classes are removed from the component.

1
2
3
4
5
6
7
8
<div id="shapes">
  <span data-reactid="0.1">
      <svg data-reactid=".0.2"
           class="">
        <circle data-reactid=".0.2"></circle>
      </svg>
  </span>
</div>

Any subsequent children will go through the same process.

Animating a component leaving is even more important. Without transition groups, a component will disappear before anything can happen to it. However, with ReactCSSTransitionGroups the whole process happens in reverse.

Let’s start with the leaving css. We’ll start at full size and shrink to nothing.

1
2
3
4
5
6
7
8
.shape-leave {
  transform: scale(1);
}

.shape-leave.shape-leave-active {
  transform: scale(0);
  transition: all 2s ease-in;
}

A component in the DOM will first receive the shape-leave class:

1
2
3
4
5
6
7
8
<div id="shapes">
  <span data-reactid="0.1">
      <svg data-reactid=".0.2"
           class="shape-leave">
        <circle data-reactid=".0.2"></circle>
      </svg>
  </span>
</div>

After that, it will receive the shape-leave-active class:

1
2
3
4
5
6
7
8
<div id="shapes">
  <span data-reactid="0.1">
      <svg data-reactid=".0.2"
           class="shape-leave shape-leave-active">
        <circle data-reactid=".0.2"></circle>
      </svg>
  </span>
</div>

And when the timeout set on the ReactCSSTransitionGroup component is reached, the element is removed. Note: The element is not removed after the animation is complete, but when the timeout is reached. So if the CSS transition is longer than the timeout it will just disappear.

1
2
3
4
<div id="shapes">
  <span data-reactid="0.1">
  </span>
</div>

And that’s all it takes.

Here’s a full demo you can try:

See the Pen ReactCSSTransitionGroup by Joe Morgan (@jsmapr1) on CodePen.