How to build a reusable animated component using React Hooks
React Hooks, introduced in version 16.8, have revolutionized how we write components. They allow us to use state and other React features in functional components, making it easier to reuse stateful logic between components.
In this post, we‘ll walk through how to leverage Hooks to create a reusable animated visibility component. We‘ll use the react-animated-css library to perform the animations and the useState and useEffect hooks to handle the component state and side effects.
The goal
Here‘s what we want our AnimatedVisibility component to do:
- Animate an element in when it becomes visible
- Animate an element out when it becomes hidden
- Toggle the display property to none once the "animating out" finishes so the element doesn‘t take up space
- Be easily reusable across different components
An example use case is a sidebar that slides in from the left when opened and slides out when closed.
Or a material design style modal that fades in and out.
Let‘s break this down step-by-step.
Setting up the animation library
We‘ll use react-animated-css to perform our animations. It‘s a simple wrapper around the popular Animate.css library.
First install the library:
npm install --save react-animated-css
Then import the Animated component and the CSS file in your component:
import { Animated } from ‘react-animated-css‘;
import ‘animate.css/animate.min.css‘;
The Animated component wraps the element you want to animate and takes a few props:
- animationIn – the animation class to apply when the element becomes visible
- animationOut – the animation class to apply when the element becomes hidden
- animationInDuration – duration of the "in" animation in milliseconds
- animationOutDuration – duration of the "out" animation in milliseconds
- isVisible – boolean determining if the element is visible or not
So to fade an element in and out we could do:
<Animated
animationIn="fadeIn"
animationOut="fadeOut"
animationInDuration={1000}
animationOutDuration={1000}
isVisible={elementIsVisible}
>
<MyComponent />
</Animated>
Toggling visibility
The Animated component handles applying the animation classes, but we still need to keep track of whether the component is visible or not and toggle that state.
We can use the useState hook in our parent component to manage this state:
const [isVisible, setIsVisible] = useState(true);
Then we pass the isVisible state to our Animated component:
<Animated
animationIn="fadeIn"
animationOut="fadeOut"
isVisible={isVisible}
>
<MyComponent />
</Animated>
Whenever we call setIsVisible(false) the component will fade out. Conversely, calling setIsVisible(true) will cause it to fade back in.
However, this introduces a problem. The element is still in the DOM after it fades out, it‘s just transparent. This can cause layout issues as the element still takes up space.
Ideally, we‘d like to set the element‘s display to none after it finishes fading out so that it‘s fully removed from the DOM.
Combining animations and display
Unfortunately, performing animations on an element and then setting its display property once the animation finishes is a bit tricky.
CSS transitions don‘t work on the display property. A common approach is to use a setTimeout to toggle display to none after the animation duration has passed.
Here‘s how we could implement this in a reusable AnimatedVisibility component using hooks:
const AnimatedVisibility = ({
visible,
children,
animationOutDuration,
disappearOffset,
...rest
}) => {
const [noDisplay, setNoDisplay] = useState(!visible);
useEffect(() => {
if (!visible) {
setTimeout(() => setNoDisplay(true), animationOutDuration + disappearOffset);
} else {
setNoDisplay(false);
}
}, [visible]);
const style = noDisplay ? { display: ‘none‘ } : null;
return (
<Animated
isVisible={visible}
style={style}
{...rest}
>
{children}
</Animated>
);
};
Let‘s break this down:
- The component takes a visible prop to control whether it‘s visible or not
- It uses an internal noDisplay state to control whether display should be set to none
- In the useEffect hook, when visible becomes false, it sets a timeout to set noDisplay to true after the animationOutDuration + a disappearOffset (to account for any delays)
- When visible is true, it ensures that noDisplay gets set to false immediately so the element shows up
- If noDisplay is true, we set the style.display to none, otherwise we don‘t apply any styles
- We pass on any other props the component receives to the Animated component
We can now use this component like:
<AnimatedVisibility
visible={sidebarIsOpen}
animationIn="slideInLeft"
animationOut="slideOutLeft"
animationInDuration={500}
animationOutDuration={500}
disappearOffset={350}
>
<MySidebar />
</AnimatedVisibility>
And our sidebar will animate in, animate out, and then have display set to none when it‘s fully hidden.
Reusing the component
With this component, it becomes trivial to add this animated visibility behavior to any element.
For example, we could reuse it for a navbar that slides down from the top:
<AnimatedVisibility
visible={navIsOpen}
animationIn="slideInDown"
animationOut="slideOutUp"
animationInDuration={300}
animationOutDuration={300}
disappearOffset={200}
>
<MyNavbar />
</AnimatedVisibility>
Or a tooltip that fades in and out:
<AnimatedVisibility
visible={tooltipIsVisible}
animationIn="fadeIn"
animationOut="fadeOut"
animationInDuration={200}
animationOutDuration={200}
disappearOffset={100}
>
<MyTooltip />
</AnimatedVisibility>
By abstracting the visibility state management and animation logic into a separate component, we can keep our individual components clean and focused.
Extracting the behavior
While the AnimatedVisibility component is already quite reusable, we can take it a step further.
Let‘s say we have a few different animation variations we want to reuse across many components, like a fade, slide left, and slide up animation.
We could create three separate components, like FadeVisibility, SlideLeftVisibility, and SlideUpVisibility that hardcode the specific animations.
But that‘s not very DRY. Ideally, we‘d like a way to compose the AnimatedVisibility behavior with any component.
With hooks, it becomes very easy to extract the behavior into a custom hook that can be added to any component.
Here‘s how we could write a useAnimatedVisibility hook:
const useAnimatedVisibility = ({
visible,
animationIn,
animationOut,
animationInDuration,
animationOutDuration,
disappearOffset
}) => {
const [noDisplay, setNoDisplay] = useState(!visible);
useEffect(() => {
if (!visible) {
setTimeout(() => setNoDisplay(true), animationOutDuration + disappearOffset);
} else {
setNoDisplay(false);
}
}, [visible]);
const style = noDisplay ? { display: ‘none‘ } : null;
return {
animationProps: {
animationIn,
animationOut,
animationInDuration,
animationOutDuration,
isVisible: visible,
style
}
};
}
This hook takes the visibility state and animation parameters and returns an object containing the necessary props to pass to the Animated component.
We can then use it in any component like:
const SlideLeftAnimatedVisibility = ({ visible, children }) => {
const { animationProps } = useAnimatedVisibility({
visible,
animationIn: ‘slideInLeft‘,
animationOut: ‘slideOutLeft‘,
animationInDuration: 500,
animationOutDuration: 500,
disappearOffset: 350
});
return (
<Animated {...animationProps}>
{children}
</Animated>
);
}
And use this component anywhere we want that specific animation:
<SlideLeftAnimatedVisibility visible={sidebarIsOpen}>
<MySidebar />
</SlideLeftAnimatedVisibility>
We could create similar components for different animation variations.
By extracting the hook, we‘ve made the animated visibility behavior even more composable and reusable across our application.
Conclusion
React Hooks enable powerful new ways to abstract and reuse component logic. By combining the useState and useEffect hooks with animation libraries, we can create reusable animated components that encapsulate the intricacies of performing animations and toggling display properties.
The useAnimatedVisibility hook and higher-order component pattern allows easily integrating these pre-configured animations into any component.
While we used react-animated-css in this example, the same concepts apply to any animation library like react-spring or framer-motion.
So next time you find yourself copying and pasting animation logic between components, see if you can abstract it into a custom hook. Your future self will thank you for keeping your component files cleaner and your animation logic all in one reusable place.