React - animate mount and unmount of a single component
Solution 1
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>
Solution 2
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.
Solution 3
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>
Solution 4
I countered this problem during my work, and simple as it seemed, it is really not in React. In a normal scenario where you render something like:
this.state.show ? {childen} : null;
as this.state.show
changes the children are mounted/unmounted right away.
One approach I took is creating a wrapper component Animate
and use it like
<Animate show={this.state.show}>
{childen}
</Animate>
now as this.state.show
changes, we can perceive prop changes with getDerivedStateFromProps(componentWillReceiveProps)
and create intermediate render stages to perform animations.
We start with Static Stage when the children is mounted or unmounted.
Once we detect the show
flag changes, we enter Prep Stage where we calculate necessary properties like height
and width
from ReactDOM.findDOMNode.getBoundingClientRect()
.
Then entering Animate State we can use css transition to change height, width and opacity from 0 to the calculated values (or to 0 if unmounting).
At the end of transition, we use onTransitionEnd
api to change back to
Static
stage.
There are much more details to how the stages transfer smoothly but this could be overall idea:)
If anyone interested, I created a React library https://github.com/MingruiZhang/react-animate-mount to share my solution. Any feedback welcome:)
Solution 5
Install framer-motion from npm.
import { motion, AnimatePresence } from "framer-motion"
export const MyComponent = ({ isVisible }) => (
<AnimatePresence>
{isVisible && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
)}
</AnimatePresence>
)
ffxsam
Updated on February 09, 2022Comments
-
ffxsam over 2 years
Something this simple should be easily accomplished, yet I'm pulling my hair out over how complicated it is.
All I want to do is animate the mounting & unmounting of a React component, that's it. Here's what I've tried so far and why each solution won't work:
ReactCSSTransitionGroup
- I'm not using CSS classes at all, it's all JS styles, so this won't work.ReactTransitionGroup
- This lower level API is great, but it requires you to use a callback when the animation is complete, so just using CSS transitions won't work here. There are always animation libraries, which leads to the next point:- GreenSock - The licensing is too restrictive for business use IMO.
- React Motion - This seems great, but
TransitionMotion
is extremely confusing and overly complicated for what I need. - Of course I can just do trickery like Material UI does, where the elements are rendered but remain hidden (
left: -10000px
) but I'd rather not go that route. I consider it hacky, and I want my components to unmount so they clean up and are not cluttering up the DOM.
I want something that's easy to implement. On mount, animate a set of styles; on unmount, animate the same (or another) set of styles. Done. It also has to be high performance on multiple platforms.
I've hit a brick wall here. If I'm missing something and there's an easy way to do this, let me know.
-
Pranesh Ravi over 7 yearsWhat kind of animation are we talking here?
-
ffxsam over 7 yearsJust something simple, like a CSS opacity fade in and a
transform: scale
-
Pranesh Ravi over 7 yearsPoint 1 and 2 confuses me. What kind of animations are you using? JS transitions or CSS transitions ?
-
ffxsam over 7 yearsDon't confuse CSS styles/classes (e.g.
.thing { color: #fff; }
) with JS styles (const styles = { thing: { color: '#fff' } }
)) -
Pranesh Ravi over 7 yearsBut the problem is, when you try to change the style using javascript, you're actually replacing the style of a element which won't give any transition.
-
Tahir Ahmed over 7 years
-
ffxsam over 7 yearsSee above, I mention GSAP/GreenSock, which I don't want to use.
-
ffxsam over 7 years@PraneshRavi (regarding replacing the style and won't transition) - That's not actually true. See this example: jsfiddle.net/mqkhh3qq
-
Tahir Ahmed over 7 years@ffxsam hmm, now I am curious what made you conclude the result that you concluded for GSAP.
-
ffxsam over 7 years@TahirAhmed GSAP costs $150/year for commercial use where you charge access to a web-based service. It's just something I'm not interested in at the moment.
-
Tahir Ahmed over 7 years
-
ffxsam over 7 years@TahirAhmed Yes, of course
-
ffxsam over 7 years@TahirAhmed " If end users are charged a usage/access/license fee, please sign up for a "Business Green" Club GreenSock membership"
-
Tahir Ahmed over 7 years@ffxsam fair enough. thanks.
-
Tahir Ahmed over 7 years@ffxsam I still have one more link to share with you :)
-
ffxsam over 7 years@TahirAhmed Oh nice! Lemme check this out in detail when I have some time
-
Webwoman almost 5 yearsConcerning ReactTransitionGroup you said:
This lower level API is great, but it requires you to use a callback when the animation is complete
, could you extrapolate what you thought about please? -
Webwoman almost 5 yearsFor thoses who want, ReactCSSTransitionGroup can work combined to styled-component
-
ffxsam over 7 yearsThanks for this! Where did you learn about
onTransitionEnd
? I don't see it in the React docs. -
Pranesh Ravi over 7 years@ffxsam facebook.github.io/react/docs/events.html It's under the transition events.
-
ffxsam over 7 yearsHow did you know what it did though, the documentation doesn't explain anything. Another question: how did you know
componentWillReceiveProps
can return something? Where can I read more on that? -
Pranesh Ravi over 7 years@ffxsam onTransitionEnd is a native JavaScript event. You can google about it. facebook.github.io/react/docs/… will give you an idea about componentWillReceiveProps.
-
ffxsam over 7 yearsI saw the documentation on
componentWillReceiveProps
, it doesn't mention returning anything. Weird. -
Pranesh Ravi over 7 years@ffxsam you won't return anything but you can update the state based on the newProp. The reason I use return is to prevent the code below return from executing. It's a better way of writing if else. With a reaturn in the if, you don't need a else statement.
-
ffxsam over 7 yearsOh I got it - you're just executing the method and returning in one shot.
-
ffxsam over 7 yearsThanks for your awesome answer, Pranesh! :) I learned a lot from this.
-
Pranesh Ravi over 7 years@ffxsam Yes, to avoid else statement. ComponentWillReceiveProps doesn't take any return value. So, it won't cause any problem.
-
ffxsam over 7 yearsBTW I think there's a mistake in your code. In your
Parent
component, you referencethis.transitionEnd
-
Ser about 6 yearsThe example above doesn't check if the
transitionEnd
event comes from that element or a child one. You should add aclassName="my-animation"
and check insidetransitionEnd(e){
thate.target.className === "my-animation"
otherwise it can be mess up -
Mingrui Zhang almost 6 yearsThank you for your feedback, sorry for crude answer earlier. I added more detail and a diagram to my answer, hope this can be more helpful to others.
-
Bugs almost 6 years@MingruiZhang It's good to see that you've taken the comments positively and improved your answer. It's very refreshing to see. Good work.
-
Micros over 5 yearsProject is no longer maintained (2018)
-
cherouvim over 5 yearsThis doesn't unmount
App
though, butApp
simply knows when to not render anything. -
Admin over 5 yearsAnd how do you mount/unmount @ffxsam?
-
Rokit over 5 yearsHow is
componentWillLeave()
andcomponentWillEnter()
getting called inAnimatedMount
? -
Webwoman about 5 yearsDoesn't works for me, here my sandbox: codesandbox.io/s/p9m5625v6m
-
Webwoman about 5 yearselegant solution, would be great if you have added some comments :)
-
Webwoman about 5 yearsalso why use typescrypt's extension since it works well in javascript's extension?
-
Webwoman about 5 yearsalso your console returns "cannot find namespace NodeJS timeout"
-
deckele about 5 years@Webwoman Thanks for your comments. I can't recreate your reported problem with "NodeJS timeout", see my CodeSandbox link below the answer. Regarding TypeScript, I personally prefer using it over JavaScript, although both are viable of course.
-
Aleks over 4 yearsIf you use styled components, you can simply pass
show
prop toH1
and do all logic inside styled component. Like...animation: ${({ show }) => show ? entranceKeyframes : exitKeyframes} 300ms ease-out forwards;
-
Scott Martin almost 4 years
componentWillReceiveProps
is now deprecated as unsafe. It would be good for someone to update this answer to usecomponentDidUpdate
. -
Slbox about 3 yearsThis can't possibly work since the methods are uncalled, and as expected, it doesn't work.
-
Marko Knöbl over 2 yearsThis solution doesn't work as intended for me. If I set the transition / timeout times to 2s / 2000ms I can clearly see that when the enter animation is triggered, the element stays hidden for 2s, and only then transitions in for 2s.
-
Marko Knöbl over 2 yearsI think this answer is outdated ... It seems like this example requires ReactTransitionGroup in the background, which used to be part of React and now has a separate package. But that package also provides Transition and CSSTransition which would be more appropriate here.
-
Viraj Singh over 2 years@Webwoman for some reason the typescript is treating the hook file as node.js file so it's forcing to let timeoutId: NodeJS.Timeout; instead of number type. try let timeoutId: ReturnType<typeof setTimeout>; or try window.setTimeout which has number type
-
Nate Levin almost 2 yearsTo fix the TypeScript error
Type 'Timeout' is not assignable to type 'number'.
, addwindow.
in front of thesetTimeout
.