DEV Community

Michal Czaplinski
Michal Czaplinski

Posted on • Edited on • Originally published at czaplinski.io

Super easy react mount/unmount animations with hooks

ORIGINAL POST: https://czaplinski.io/blog/super-easy-animation-with-react-hooks/ (has better formatting and syntax highlighting)

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");
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode
// index.js

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

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);
Enter fullscreen mode Exit fullscreen mode
// 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;
Enter fullscreen mode Exit fullscreen mode
/* styles.css */

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

@keyframes fadeOut {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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);
  // ...
}
Enter fullscreen mode Exit fullscreen mode

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>
    )
  );
// ...
Enter fullscreen mode Exit fullscreen mode

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);
  };
Enter fullscreen mode Exit fullscreen mode

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!!! ✌️

Top comments (9)

Collapse
 
laurencefass profile image
laurence fass

hi this doesnt work if i add a transition delay. it renders the child and then executes the animation after the delay. is there a way to fix this?

animation: ${show ? "fadeIn" : "fadeOut"} 1s linear 1s,

results in: codesandbox.io/s/react-easy-animat...

Collapse
 
michalczaplinski profile image
Michal Czaplinski

Hi there!

The reason that this does not work as you expect is because the component is mounted and visible when it's waiting that one second for the animation to start. The 1s that you have added does not control whether the component is visible or not - it only delays the animation!

I would recommend that you add a setTimeout in the useEffect of the Fade component to delay mounting the children by your desired amount of time!

// Fade.js

  useEffect(() => {
    const timeout = setTimeout(() => {
      if (show) setRender(true);
    }, 1000); // <---- here you set your delay in miliseconds

    // Clear the timeout when the component unmounts 
    return () => clearTimeout(timeout);
  }, [show]);
Enter fullscreen mode Exit fullscreen mode

And here's the full codesandbox with the 1s delay: codesandbox.io/s/react-easy-animat...

Collapse
 
zyinmd profile image
Zhi Yin

If when I switch from rendering big component A to rendering big component B, and it takes 2 seconds for B to appear, so I want to add some fade animation to make the user feel it's more responsive. In my case, if I fade out A, does B start being calculated from start of fade or end of fade?

Collapse
 
michalczaplinski profile image
Michal Czaplinski

Hi! Sorry for the late reply, I must have missed your comment earlier.

The code that I have shown above is not appropriate for the use case that you have described. Notice, that I am not fading in and out two different components A and B. I am wrapping a component A with a so that I can fade it in and out when I want. So, there is no notion of "start of fade" and "end of fade" - you would have to create that yourself.

As a side note: I don't believe you would ever have a use case like you described. No component (no matter how big) should ever take 2 seconds to render (not even at facebook scale). The only case when that should happen is if the component is fetching some data after having been mounted. But this would be unrelated to the fading mechanism that I described above.

Hope this helps, let me know if you have more questions!

Collapse
 
kristoferek profile image
Christopher Michael Nowak

Hi, thanks for baby simple and straightforward explanation.

Collapse
 
michalczaplinski profile image
Michal Czaplinski

Glad you enjoyed it!!! :)

Collapse
 
evgenyartemov profile image
Evgeny Artemov

Great Solution!!! Thanks!!!

Collapse
 
cameronapak profile image
cameronapak

I was trying to figure this out myself. Great idea!

Collapse
 
minhhunghuynh1106 profile image
igdev

If I use with onTransitionEnd not onAnimationEnd it's not work when component mounted. Can u show me a simple example with transition? Thank u so much for this article! 😍