React - animate mount and unmount of a single component
This is a bit lengthy but I've used all the native events and methods to achieve this animation. No ReactCSSTransitionGroup
, ReactTransitionGroup
and etc.
Things I've used
- React lifecycle methods
-
onTransitionEnd
event
How this works
- Mount the element based on the mount prop passed(
mounted
) and with default style(opacity: 0
) - After mount or update, use
componentDidMount
(componentWillReceiveProps
for further updates)to change the style (opacity: 1
) with a timeout(to make it async). - During unmount, pass a prop to the component to identify unmount, change the style again(
opacity: 0
),onTransitionEnd
, remove unmount the element from the DOM.
Continue the cycle.
Go through the code, you'll understand. If any clarification is needed, please leave a comment.
Hope this helps.
class App extends React.Component{
constructor(props) {
super(props)
this.transitionEnd = this.transitionEnd.bind(this)
this.mountStyle = this.mountStyle.bind(this)
this.unMountStyle = this.unMountStyle.bind(this)
this.state ={ //base css
show: true,
style :{
fontSize: 60,
opacity: 0,
transition: 'all 2s ease',
}
}
}
componentWillReceiveProps(newProps) { // check for the mounted props
if(!newProps.mounted)
return this.unMountStyle() // call outro animation when mounted prop is false
this.setState({ // remount the node when the mounted prop is true
show: true
})
setTimeout(this.mountStyle, 10) // call the into animation
}
unMountStyle() { // css for unmount animation
this.setState({
style: {
fontSize: 60,
opacity: 0,
transition: 'all 1s ease',
}
})
}
mountStyle() { // css for mount animation
this.setState({
style: {
fontSize: 60,
opacity: 1,
transition: 'all 1s ease',
}
})
}
componentDidMount(){
setTimeout(this.mountStyle, 10) // call the into animation
}
transitionEnd(){
if(!this.props.mounted){ // remove the node on transition end when the mounted prop is false
this.setState({
show: false
})
}
}
render() {
return this.state.show && <h1 style={this.state.style} onTransitionEnd={this.transitionEnd}>Hello</h1>
}
}
class Parent extends React.Component{
constructor(props){
super(props)
this.buttonClick = this.buttonClick.bind(this)
this.state = {
showChild: true,
}
}
buttonClick(){
this.setState({
showChild: !this.state.showChild
})
}
render(){
return <div>
<App onTransitionEnd={this.transitionEnd} mounted={this.state.showChild}/>
<button onClick={this.buttonClick}>{this.state.showChild ? 'Unmount': 'Mount'}</button>
</div>
}
}
ReactDOM.render(<Parent />, document.getElementById('app'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.2/react-with-addons.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>
Here is my solution using the new hooks API (with TypeScript), based on this post, for delaying the component's unmount phase:
function useDelayUnmount(isMounted: boolean, delayTime: number) {
const [ shouldRender, setShouldRender ] = useState(false);
useEffect(() => {
let timeoutId: number;
if (isMounted && !shouldRender) {
setShouldRender(true);
}
else if(!isMounted && shouldRender) {
timeoutId = setTimeout(
() => setShouldRender(false),
delayTime
);
}
return () => clearTimeout(timeoutId);
}, [isMounted, delayTime, shouldRender]);
return shouldRender;
}
Usage:
const Parent: React.FC = () => {
const [ isMounted, setIsMounted ] = useState(true);
const shouldRenderChild = useDelayUnmount(isMounted, 500);
const mountedStyle = {opacity: 1, transition: "opacity 500ms ease-in"};
const unmountedStyle = {opacity: 0, transition: "opacity 500ms ease-in"};
const handleToggleClicked = () => {
setIsMounted(!isMounted);
}
return (
<>
{shouldRenderChild &&
<Child style={isMounted ? mountedStyle : unmountedStyle} />}
<button onClick={handleToggleClicked}>Click me!</button>
</>
);
}
CodeSandbox link.
Using the knowledge gained from Pranesh's answer, I came up with an alternate solution that's configurable and reusable:
const AnimatedMount = ({ unmountedStyle, mountedStyle }) => {
return (Wrapped) => class extends Component {
constructor(props) {
super(props);
this.state = {
style: unmountedStyle,
};
}
componentWillEnter(callback) {
this.onTransitionEnd = callback;
setTimeout(() => {
this.setState({
style: mountedStyle,
});
}, 20);
}
componentWillLeave(callback) {
this.onTransitionEnd = callback;
this.setState({
style: unmountedStyle,
});
}
render() {
return <div
style={this.state.style}
onTransitionEnd={this.onTransitionEnd}
>
<Wrapped { ...this.props } />
</div>
}
}
};
Usage:
import React, { PureComponent } from 'react';
class Thing extends PureComponent {
render() {
return <div>
Test!
</div>
}
}
export default AnimatedMount({
unmountedStyle: {
opacity: 0,
transform: 'translate3d(-100px, 0, 0)',
transition: 'opacity 250ms ease-out, transform 250ms ease-out',
},
mountedStyle: {
opacity: 1,
transform: 'translate3d(0, 0, 0)',
transition: 'opacity 1.5s ease-out, transform 1.5s ease-out',
},
})(Thing);
And finally, in another component's render
method:
return <div>
<ReactTransitionGroup>
<Thing />
</ReactTransitionGroup>
</div>