DEV Community

Cover image for React animation onDestroy (removed from the DOM)
Arnau Espin
Arnau Espin

Posted on

React animation onDestroy (removed from the DOM)

Hello and welcome to this post about animating a transition in React when a component is destroyed, AKA removed from the DOM.

I usually face issues when applying some fadeIn/fadeOut transition on a React component or JSX element. Because when the component/element is removed from the DOM I can no longer apply any transition.

Let's write some basic React app with a counter which is increased by 1 every second.
We display the counter and below it we display a react image whenever the count is divisible by 3.

Demo here.

// Disable React Strict mode for this example
function App() {
  const [count, setCount] = useState(0);
  const [isDivisibleByThree, setIsDivisibleByThree] = useState(false);

  useEffect(() => {
    if (count !== 0 && count % 3 === 0) setIsDivisibleByThree(true);
  }, [count]);

  useEffect(() => {
    setInterval(() => {
      setCount((p) => (p += 1));
    }, 1000);
  }, []);

  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        gap: "50px",
        alignItems: "center",
        justifyContent: "center",
      }}
    >
      <div>
        <h1>count is {count}</h1>
      </div>
      <p>
        {isDivisibleByThree && (
          <img ref={imageRef} src={reactLogo} alt="react logo"/>
        )}
      </p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

We can animate whenever the <img> is visible with the following CSS.
This way when the image first appears it will have some slide-in animation, moving from top to bottom.

img {
  animation: slide-in 0.5s ease-in-out;
}
p {
  height: 5rem;
  width: 5rem;
}
@keyframes slide-in {
  0% {
    opacity: 0;
    transform: translateY(-50%);
  }
  100% {
    opacity: 1;
    transform: translateY(0);
  }
}
Enter fullscreen mode Exit fullscreen mode

So... how can we animate when the <img> element is removed from the DOM.

Let's create a custom hook called useHandleDestroyAnimated, this hook accepts a ref (from the useRef hook) from the HTML element we wish to animate on destroy.

Before hiding the <img> through setting the isDivisibleByThree state to false, we animate its destruction after 1000ms, then, we set the state to false.

async function sleep(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function useHandleDestroyAnimated<T extends HTMLElement>(
  ref: MutableRefObject<T | null>
): [boolean, (_: boolean) => void] {
  const [state, setState] = useState(false);

// everytime the state or ref change, if state is true, we animate the destroy of the component.
  useEffect(() => {
    if (state) {
      handleDeletion(ref);
    }
  }, [ref, state]);

  function handleDeletion<T extends HTMLElement>(
    element: MutableRefObject<T | null>
  ) {
    const style = element?.current?.style;
    if (!style) return;
    sleep(1000).then(() => {
      style.transition = "all 0.5s";
      style.transform = "translateY(-50%)";
      style.opacity = "0";
      sleep(1000).then(() => {
        setState(false);
      });
    });
  }
  return [state, setState];
}
Enter fullscreen mode Exit fullscreen mode

Let's add the ref from the useRef hook to the App.tsx:

const imageRef = useRef<HTMLImageElement | null>(null);
.....
.....
.....
 <p>
        {isDivisibleByThree && (
          <img ref={imageRef} src={reactLogo} alt="react logo" />
        )}
      </p>
Enter fullscreen mode Exit fullscreen mode

The final code is:

import { useEffect, useState, MutableRefObject, useRef } from "react";
import reactLogo from "./assets/react.svg";
import "./App.css";

async function sleep(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

const styles = Object.freeze({
  opacity: "0",
  transform: "translateY(-50%)",
  transition: "all 0.5s",
});

function useHandleDestroyAnimated<T extends HTMLElement>(
  ref: MutableRefObject<T | null>
): [boolean, (_: boolean) => void] {
  const [state, setState] = useState(false);

  useEffect(() => {
    if (state) {
      handleDeletion(ref);
    }
  }, [ref, state]);

  function handleDeletion<T extends HTMLElement>(
    element: MutableRefObject<T | null>
  ) {
    const style = element?.current?.style;
    if (!style) return;
    sleep(1000).then(() => {
      style.transition = styles.transition;
      style.transform = styles.transform;
      style.opacity = styles.opacity;
      sleep(1000).then(() => {
        setState(false);
      });
    });
  }
  return [state, setState];
}

function App() {
  const [count, setCount] = useState(0);
  const imageRef = useRef<HTMLImageElement | null>(null);
  const [isDivisibleByThree, setIsDivisibleByThree] =
    useHandleDestroyAnimated(imageRef);

  useEffect(() => {
    if (count !== 0 && count % 3 === 0) setIsDivisibleByThree(true);
  }, [count]);

  useEffect(() => {
    setInterval(() => {
      setCount((p) => (p += 1));
    }, 1000);
  }, []);

  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        gap: "50px",
        alignItems: "center",
        justifyContent: "center",
      }}
    >
      <div>
        <span>count is {count}</span>
      </div>
      <p>
        {isDivisibleByThree && (
          <img ref={imageRef} src={reactLogo} alt="react logo" />
        )}
      </p>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

I hope you found this useful!

You can follow me on:

Top comments (0)