DEV Community

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

Posted on

7 1

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:

Sentry blog image

How I fixed 20 seconds of lag for every user in just 20 minutes.

Our AI agent was running 10-20 seconds slower than it should, impacting both our own developers and our early adopters. See how I used Sentry Profiling to fix it in record time.

Read more

Top comments (0)

AWS GenAI LIVE image

Real challenges. Real solutions. Real talk.

From technical discussions to philosophical debates, AWS and AWS Partners examine the impact and evolution of gen AI.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay