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>
);
}
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);
}
}
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];
}
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>
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;
I hope you found this useful!
You can follow me on:
Top comments (0)