Animate React component by calling 'setState' in 'componentDidMount'

On the frontend, we do a lot of animations. Most of the simple animations I create by using CSS transitions. Either I will change class or inline style of the element, and define transitions in CSS file.

Easiest way to do this in React is to render initial state, and then when it renders, change the state to apply class or style to animate. The easiest way to do it in React is to change state in componentDidMount. Setting state in componentDidMount is considered to be anti-pattern, as it forces rerender and can lead to property/layout thrashing. But in our case, that is exactly what we want to do.

When we do that, we hit the wall - only second state is rendered and there is no transition between two states. It happens because of browsers optimization - browsers are not rerendering stuff that changed in the same animation frame. But they merge changes and render the end result.

The problem I just described is not React exclusive, but browser related. Same will happen if we try something like this:

const element = document.querySelector('.AnimateMe');
element.style.height = '50px';
element.style.height = '250px';

So let's start with example of the problem.

Animate component by changing state in componentDidMount doesn't work

The scenario I described above. Element height depends on this.state.animation property. It is initially set to false and element height should be 50px. In componentDidMount we'll change the value of this.props.animation to true and element height should be 250px. As we added transition, it should animate.

But it doesn't as all of this happens really fast, and browser decides to merge changes and render only the end result. This way our element immediately gets height of 250px.

import React, { Component } from 'react';

export default class AnimateMe extends Component {
  constructor(props) {
    super(props);

    this.state = {
      animate: false,
    };
  }

  componentDidMount() {
    this.setState({ animate: true });
  }

  render() {
    return (
      <div
        style={ {
          background: '#eee',
          border: '1px solid black',
          height: this.state.animate ? 250 : 50,
          margin: 20,
          padding: 20,
          transition: 'all 2s',
        } }
      >
        Animate my height
      </div>
    );
  }
}

Solution, add a short timeout

I've run multiple times into this problem, but I was lazy to dig deeper and find the real reason why it is happening. My solution was to add a timeout, first I tried immediate timeout (setTimeout(fn, 0)), but alas, it didn't work in some browsers (as it happened too fast again and browser did their optimizations). Then I increased it to the magical value 50 (setTimeout(fn, 50)), and it worked in every browser.

Now I get the reason why it works. Because 50ms is larger than animation frame (which is around 16ms to achieve 60fps).

(I'll show you just the part of the code that is changed)

...
  componentDidMount() {
    // Added timeout
    const ANIMATION_TIMEOUT = 50;

    this.setTimeout(() => {
      this.setState({ animate: true });
    }, ANIMATION_TIMEOUT);
  }
...

So this is cross browser solution, but I always cringe a little when I'm forced to use timeouts like this. And I have been talking about animation frames a lot, so why don't we try that next?

Using requestAnimationFrame instead on timeout

We'll just replace timeout with requestAnimationFrame and it should work. But not in Firefox :( to make things worse, sometimes it does, and sometimes doesn't. My guess that sometimes it gets squeezed in to the same animation frame.

Update, October 2017

Hooray! Firefox fixed this one, in newer versions it works without two nested requestAnimationFrame. But you might want to stick with it for some time, to make sure all of your users upgraded their browsers.

(Again just the part of the code that is changed)

...
  componentDidMount() {
    // Added requestAnimationFrame
    requestAnimationFrame(() => {
      this.setState({ animate: true });
    });
  }
...

So now we need to make sure our two states belong to different animation frames each.

Nested requestAnimationFrame to the rescue

Idea is to separate renders of two states to different animation frames. As we are not going to wrap React's render method into animation frame, we need to nest them instead in componentDidMount.

This way we guarantee first render won't be merged together with the second one. Problem we had in Firefox is now gone.

This looks hacky, but I think it is a legit solution. Instead of trying to get a magic value for timeout, just use native methods that browsers provide.

(Again just the part of the code that is changed)

...
  componentDidMount() {
    // Added two nested requestAnimationFrames
    requestAnimationFrame(() => {
      // Firefox will sometimes merge changes that happened here
      requestAnimationFrame(() => {
        this.setState({ animate: true });
      });
    });
  }
...

Finally, put it in a helper

To make things a bit cleaner, I extract this to a helper function and then use whenever I need it. This can be used in our React examples but also with any other framework or vanilla JavaScript.

If you support browsers without requestAnimationFrame be sure to polyfill it. Paul Irish has a great one here.

// Start animation helper using nested requestAnimationFrames
function startAnimation(callback) {
  requestAnimationFrame(() => {
    requestAnimationFrame(() => {
      callback();
    });
  });
}

(Our componentDidMount code using helper)

...
  componentDidMount() {
    // You'll need to import startAnimation at the top of the file
    startAnimation(() => {
      this.setState({ animate: true });
    });
  }
...

At the end

Hope you learned something, and I'm interested to hear if somebody is using different approach to solve this problem. Cheers!

Comments (8)

Ariel
04. Sep 2017, 21:51

This tutorial is amazing. You are amazing.

I regularly read tutorials that leave way too much out or simply don't explain how they got to their solution.

This is spot on.

Love it! Please keep doing your thing!

Stanko
05. Sep 2017, 06:06

You are welcome :) And thank you, I'm doing my best to write about real problems I encounter.

Cheers!

Batu
12. Sep 2017, 08:31

Thank you!

Jimmy
02. Oct 2017, 20:05

Not sure why – but this method didn't work for me.

This appears to be the pattern at the moment: https://reactjs.org/docs/animation.html

Stanko
03. Oct 2017, 07:48

Hello Jimmy, I'm sorry to hear it didn't work for you. Method works for sure, as you can see from the examples. I used it numerous times. You can check react-animate-height which is using it as well.

ReactTransitionGroup and ReactCSSTransitionGroup are out for a while, but they do a little bit different thing.

Method I described is meant for a simple two-states animations. For such things I prefer doing it on my own rather than pulling whole library. Plus it gives you more control.

Cheers!

Benjamin
27. Dec 2017, 06:24

I came here because animationFrame was updating too fast, and setting timeouts made it look choppy. Your double requestAnimationFrame did the trick. Would never have thought of that.

Aswin
16. Jun 2018, 11:45

Hi Stanko, Nice and clear write up. I have described my problem in the below "so" post. https://stackoverflow.com/questions/50881720/making-dom-updates-in-millisecond-intervals. Is rAF the way forward for websocket data too ?

Daniel González González
22. Oct 2018, 23:43

Beautiful tutorial :)! Easy to adapt to different use cases. Thanks Stanko!