Search code examples
animationreactjscss-animationsgsapreact-motion

React - animate mount and unmount of a single component


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:

  1. ReactCSSTransitionGroup - I'm not using CSS classes at all, it's all JS styles, so this won't work.
  2. 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:
  3. GreenSock - The licensing is too restrictive for business use IMO.
  4. React Motion - This seems great, but TransitionMotion is extremely confusing and overly complicated for what I need.
  5. 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.


Solution

  • 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.

    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>