React - animate mount and unmount of a single component

ffxsam picture ffxsam · Oct 15, 2016 · Viewed 68.9k times · Source

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.

Answer

Pranesh Ravi picture Pranesh Ravi · Oct 21, 2016

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>