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

    How this works

    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>