简体   繁体   中英

React smooth transition between different states in a component

I have a simple component like this:

var component = React.createClass({
  render: function(){
      if (this.props.isCollapsed){
         return this.renderCollapsed();
      }
      return this.renderActive()
  },
  renderActive: function(){
    return (
      <div>
      ...
      </div>
    );
  },
  renderCollapsed: function(){
    return (
      <div>
      ...
      </div>
    );
  },
});

Basically, when the property changes, the component will either show active state or collapse state.

What I am thinking is, when the property change happens, ie active->collapse, or the other way around, I want the old view "shrink" or "expand" smoothly to show the new view. For example, if it is active -> collapse, I want the active UI to shrink to the size of collapse UI, and show it smoothly.

I am not sure how to achieve this effect. Please share some ideas. Thanks!

Rather than conditionally render two different end states of a component, you could instead
toggle the class on the same component. You could have active and collapsed classes as follows:

For example:

.active{
  -webkit-transition: -webkit-transform .5s linear;  // transition of 
                                                     // 0.5 of a second
  height: 200px;
}

.collapsed{
  height: 0px;
}

Check out this resource for examples

Here is a minimal working example:

 const collapsible = ({active, toggle}) => <div> <button type="button" onClick={toggle}>Toggle</button> <div className={'collapsible' + (active? ' active': '')}> text </div> </div> const component = React.createClass({ getInitialState() { return {active: false} }, toggle() { this.setState({active: !this.state.active}) }, render() { return collapsible({active: this.state.active, toggle: this.toggle}) } }) ReactDOM.render(React.createElement(component), document.querySelector('#root')) 
 .collapsible { height: 1.5rem; transition: height 0.25s linear; background: #333; border-radius: 0.25rem } .collapsible.active { height: 7rem } 
 <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.js"></script> <div id="root"></div> 

The view can be "shrink" or "expand" smoothly by CSS transition , which is triggered by changing CSS properties.

To control CSS properties with React, we can reflect state changes to property values or className in render() .

In this example, .active class affects height value, and is controlled by state.active . The class is toggled by React in response to state changes, and triggers CSS transition.

For smoother transitions, See this article .

The standard way is to use CSSTransitionGroup from react-transition-group , which is quite easy. Wrap the component with the CSSTransitionGroup and set timeouts on enter and leave, like this:

<CSSTransitionGroup
      transitionName="example"
      transitionEnterTimeout={500}
      transitionLeaveTimeout={300}>
      {items}
</CSSTransitionGroup>

From the v1-stable docs :

"In this component, when a new item is added to CSSTransitionGroup it will get the example-enter CSS class and the example-enter-active CSS class added in the next tick."

Add styling for the CSS classes to get the correct animation.

There's also pretty good explanation in React docs , check it out.

There are third-party components for animation as well.

One more approach to this situation might be changing state after animation completes. The benefits of it is that you can apply not only transitions but whatever actions you want (js animations, smil, etc ..), main thing is not to forget to call an end callback;)

Here is working example CodePen

And here is the code example:

const runTransition = (node, {property = 'opacity', from, to, duration = 600, post = ''}, end) => {
  const dif = to - from;
  const start = Date.now();

  const animate = ()=>{
    const step = Date.now() - start;
    if (step >= duration) {
      node.style[property] = to + post;
      return typeof end == 'function' && end();
    }

    const val =from + (dif * (step/duration));
    node.style[property] =  val + post;
    requestAnimationFrame(animate);
  }

  requestAnimationFrame(animate);

}

class Comp extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isCollapsed: false
    }

    this.onclick = (e)=>{
       this.hide(e.currentTarget,()=>{
         this.setState({isCollapsed: !this.state.isCollapsed})
       });
     };

    this.refF = (n)=>{
       n && this.show(n);
     };
  }

  render() {
    if (this.state.isCollapsed){
         return this.renderCollapsed();
      }
      return this.renderActive()
  }

  renderCollapsed() {
    return (
      <div 
        key='b'
        style={{opacity: 0}}
        ref={this.refF}
        className={`b`}
        onClick={this.onclick}>
          <h2>I'm Collapsed</h2>
      </div>
      )
  }

  renderActive() {
    return (
      <div 
        key='a'
        style={{opacity: 0}}
        ref={this.refF}
        className={`a`}
        onClick={this.onclick}>
          <h2>I'm Active</h2>
      </div>
      )
  }

  show(node, cb)  {
    runTransition(node, {from: 0, to: 1}, cb);
  }

  hide(node, cb) {
    runTransition(node, {from: 1, to: 0}, cb);
  }

}

ReactDOM.render(<Comp />, document.getElementById('content'));

And for sure, for this approach to work your only opportunity is to relay on state, instead of props of Component, which you can always set in componentWillReceiveProps method if you have to deal with them.

Updated

Codepen link updated with more clear example which is showing benefits of this approach. Transition changed to javascript animation, without relying on transitionend event.

You can add a class that describes the active state ie. .active and toggle that class when switching states.

The css should look something like this:

.your-component-name{
  // inactive css styling here
}

.your-component-name.active {
  // active css styling here
}

As you want to render two different component for each of active and collapsed, wrap them in a div that controls the height with the help of CSS.

render: function(){
var cls = this.props.isCollapsed() ? 'collapsed' : 'expanded';
return(
  <div className={cls + ' wrapper'}>
    {
      this.props.isCollapsed() ?
       this.renderCollapsed() :
       this.renderActive()
    }
  </div>
);
}

and in your CSS:

.wrapper{
  transition: transform .5s linear;
}

.expanded{  
  height: 200px;
}

.collapsed{
  height: 20px;
}

Here you have a Toggle react component using the Velocity-React library, which is great for giving animations to transitions in React uis:

import React, { Component } from 'react';
import { VelocityTransitionGroup } from 'velocity-react';

export default class ToggleContainer extends Component {
    constructor () {
        super();

        this.renderContent = this.renderContent.bind(this);
    }

    renderContent () {
        if (this.props.show) {
            return (
                <div className="toggle-container-container">
                    {this.props.children}
                </div>
            );
        }
        return null
    }

    render () {
        return (
            <div>
                <h2 className="toggle-container-title" onClick={this.props.toggle}>{this.props.title}</h2>
                <VelocityTransitionGroup component="div" enter="slideDown" leave="slideUp">
                    {this.renderContent()}
                </VelocityTransitionGroup>
            </div>
        );
    }

};

ToggleContainer.propTypes = {
    show: React.PropTypes.bool,
    title: React.PropTypes.string.isRequired,
    toggle: React.PropTypes.func.isRequired,
};

Hope it helps!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM