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");
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:
- When the
show
prop changes, don't unmount just yet, but "schedule" an unmount. - Start the unmount animation.
- 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!!! ✌️
Top comments (9)
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...
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 theuseEffect
of theFade
component to delay mounting the children by your desired amount of time!And here's the full codesandbox with the 1s delay: codesandbox.io/s/react-easy-animat...
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?
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!
Hi, thanks for baby simple and straightforward explanation.
Glad you enjoyed it!!! :)
Great Solution!!! Thanks!!!
I was trying to figure this out myself. Great idea!
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! 😍