Mounting and un-mounting a React component with animation

Handling animations in React can be tricky when you start considering the behavior of the mounting and un-mounting.
My objective was to create an easy way to handle both during a CSS transition. Animate the component when rendered, and un-mounting after the "outro" animation ended.
There are a couple of libraries out there but most seemed overkill, so I created a HOC to handle this.
This is what I have in mind when trying to create a HOC:
<Block mount={toggle} onMount="in" onUnmount="out" defaultClass="wrapper" animateOnLoad={false}>
Animated Block
</Block>A couple of properties passed to defined the animation, and other to define the behavior of the component:
mountwould control wether the component should be mounted or not. Similar to how we use conditional rendering.- Both
onMountandonUnmountwould contain the CSS classes that have the corresponding keyframe defined on itsanimationproperty. defaultClassis optional, and it's there to set the styles on the component being wrapped.animateOnLoadcontrols if we want the component to be animated when first rendering it.
The HOC itself would handle 3 states:
mountwould be directly in sync with the property.inProgressto check if any of the animations has not finishedblockAnimationwould prevent the animation from running again after finished.
state
For the initial state, mount would match whatever was passed as property. inProgress would be defined based on animateOnLoad (which only matters the first time we render the component, really), and blockAnimation would be false if we're planning to animate the component on load.
state = {
mount: this.props.mount,
inProgress: this.props.mount && this.props.animateOnLoad,
blockAnimation: !this.props.animateOnLoad
};getDerivedStateFromProps()
We mentioned before that only mount state would be in sync with the property, but inProgress is also affected by this. Whenever the mount changes, inProgress should reset to true , since the animation (either for mounting or un-mounting) is planned to be executed. All this should also consider the blockAnimation state for that first mounting animation.
static getDerivedStateFromProps(props, state) {
if (!state.blockAnimation && props.mount !== state.mount) {
return {
inProgress: true,
mount: props.mount
};
}return null;
}
shouldComponentUpdate()
The HOC should also update based on the blockAnimation state.
shouldComponentUpdate(nextProps, nextState) {
return this.state.blockAnimation === nextState.blockAnimation;
}render()
The HOC's render function will set the animation classes, and handling the mounting of the component. The animation is handled by a div wrapping our component.
Notice how we listen to onAnimationEnd and have a React reference on it, both will be explained next.
render() {
const { mount, inProgress, blockAnimation } = this.state;
const { onMount, onUnmount, defaultClass } = this.props;
const animationClass = mount ? onMount : onUnmount;return (inProgress || mount) ? (
<div
ref={this.wrapperRef}
className={`${defaultClass} ${!blockAnimation ? animationClass : ''}`}
onAnimationEnd={this.onAnimationEnd}
>
<WrappedComponent {...this.props} />
</div>
) : null;
}
};
onAnimationEnd()
This just updates inProgress after the animation has finished. The reference is just to make sure that the event.target matches our div wrapper.
onAnimationEnd = event => {
const { target } = event;
const { current } = this.wrapperRef;if (target === current) {
this.setState({
inProgress: false
});
}
};
componentDidMount()
Finally, when mounted, we can turn off that blockAnimation flag
componentDidMount() {
this.setState({
blockAnimation: false
});
}
I made a repository with this (and a npm package) so you can make use of the HOC. I worked on this while learning React, so I'm sure there are libraries more optimized for this kind of tasks.
