Michal Czaplinski

Super easy react mount/unmount animations with hooks

July 05, 20198 minute read ⏱

One of the main use cases for animations on the web is simply adding and removing elements from the page. However, doing that in react can be a pain in the ass because we cannot directly manipulate the DOM elements! Since we let react take care of rendering, we are forced to do animations the react-way. When faced with this revelation, some developers begin to miss the olden days of jQuery where you could just do:

$("#my-element").fadeIn("slow");

In case you are wondering what that the difficulty is exactly, let me illustrate with a quick example:

/* styles.css */

@keyframes fadeIn {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}
// index.js

const App = ({ show = true }) => (
  show 
  ? <div style={{ animation: `fadeIn 1s` }}>HELLO</div> 
  : null
)

This is all we need to animate mounting of the component with a fadeIn, but there is no way to animate the unmounting, because we remove the the <div/> from the DOM as soon as the show prop changes to false! The component is gone and there is simply no way animate it anymore. What can we do about it? 🤔

Basically, we need to tell react to:

  1. When the show prop changes, don’t unmount just yet, but “schedule” an unmount.
  2. Start the unmount animation.
  3. As soon as the animation finishes, unmount the component.

I want to show you the simplest way to accomplish this using pure CSS and hooks. Of course, for more advanced use cases there are excellent libraries like react-spring.

For the impatient, here’s the code, divided into 3 files:

// index.js

import React, { useState } from "react";
import ReactDOM from "react-dom";

import "./styles.css";
import Fade from "./Fade";

const App = () => {
  const [show, setShow] = useState(false);

  return (
    <div>
      <button onClick={() => setShow(show => !show)}>
        {show ? "hide" : "show"}
      </button>
      <Fade show={show}>
        <div> HELLO </div>
      </Fade>
    </div>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
// Fade.js

import React, { useEffect, useState } from "react";

const Fade = ({ show, children }) => {
  const [shouldRender, setRender] = useState(show);

  useEffect(() => {
    if (show) setRender(true);
  }, [show]);

  const onAnimationEnd = () => {
    if (!show) setRender(false);
  };

  return (
    shouldRender && (
      <div
        style={{ animation: `${show ? "fadeIn" : "fadeOut"} 1s` }}
        onAnimationEnd={onAnimationEnd}
      >
        {children}
      </div>
    )
  );
};

export default Fade;
/* styles.css */

@keyframes fadeIn {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

@keyframes fadeOut {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

Let’s break down what’s going on here, starting with the first file. The interesting part is this:

// index.js

const App = () => {
  const [show, setShow] = useState(false);

  return (
    <div>
      <button onClick={() => setShow(show => !show)}>
        {show ? "hide" : "show"}
      </button>
      <Fade show={show}>        <div> HELLO </div>      </Fade>    </div>
  );
};

We simply pass a show prop which controls whether to show the children of the <Fade /> component. The rest of the code in this component is just managing the hiding/showing using the useState hook.

<Fade/> component receives 2 props: show and children. We use the value of the show prop to initialize the shouldRender state of the <Fade /> component:

// Fade.js

const Fade = ({ show, children }) => {
  const [shouldRender, setRender] = useState(show);
  // ...
}

This gives use a way to separate the animation from the mounting/unmounting.

The show prop controls whether we apply the fadeIn or fadeOut animation and the shouldRender state controls the mounting/unmounting:

// ...
return (
    shouldRender && (      <div
        style={{ animation: `${show ? "fadeIn" : "fadeOut"} 1s` }}        onAnimationEnd={onAnimationEnd}
      >
        {children}
      </div>
    )
  );
// ...

You can recall from before that our main problem was that react will unmount the component at the same time as we try to apply the animation, which results in the component disappearing immediately. But now we have separated those two steps!

We just need a way to tell react to sequence the fadeOut animation and the unmounting and we’re done! 💪

For this, we can use the onAnimationEnd event. When the animation has ended running and the component should be hidden (show === false) then set the shouldRender to false!

const onAnimationEnd = () => {
    if (!show) setRender(false);
  };

The whole example is also on Codesandbox where you can play around with it!

Hey! 👋 Before you go! 🏃‍♂️

If you enjoyed this post, you can follow me on twitter for more programming content or drop me an email 🙂

I absolutely love comments and feedback!!! ✌️


Michal Czaplinski
🌴 I'm a freelance web engineer who loves great UX.
🇵🇪 Originally from Poland, currently living in Lima, Peru.