React animate transition between components
Asked Answered
M

3

19

I'd like to animate between two components where the first component fades out and is removed from the DOM before the next component is added to the DOM and fades in. Otherwise, the new component is added to the DOM and takes up space before the old component is removed. You can see the problem in this fiddle:

http://jsfiddle.net/phepyezx/4

// css snippet
.switch-enter {
    opacity: 0.01;
}
.switch-enter.switch-enter-active {
    opacity: 1.0;
}
.switch-leave {
    opacity: 1.0;
}
.switch-leave.switch-leave-active {
    opacity: 0;
}

// React snippet 
<ReactCSSTransitionGroup transitionName="switch">
    <div key={key} className={className}>{this.text()}</div>
</ReactCSSTransitionGroup>

An unacceptable solution (for me) is to hide the original component with css before transitioning to the new component as seen here:

http://jsfiddle.net/phepyezx/5

// Change to css
.switch-leave {
    visibility: hidden;
    height: 0px;
    width: 0px;
    opacity: 1.0;
}

Is there a way to "delay" react from mounting a new component before the original is removed? I'm open to velocity or some other library to achieve this.

Thanks

Mendive answered 29/10, 2015 at 23:39 Comment(0)
M
12

Solved using the componentWillUnmount() lifecycle method.

http://jsfiddle.net/phepyezx/9/

Here's the code:

var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;

const Off = React.createClass({
    componentWillUnmount () {
        this.props.handleTransitionEnd();
    },
    render()  {
        return (
            <div className="off button">OFF</div>
        )
    }
});

const On = React.createClass({
    componentWillUnmount () {
        this.props.handleTransitionEnd();
    },
    render()  {
        return (
            <div className="on button">ON</div>
        )
    }
});

var Switch = React.createClass({
    getInitialState: function() {
        return {
            on: false,
            transitionEnd: true
        };
    },

    toggle: function(e) {
        this.setState({
            on: !this.state.on,
            transitionEnd: false
        });
    },

    handleTransitionEnd() {
        this.setState({transitionEnd: true});
    },

    renderOff() {
        if (! this.state.on && this.state.transitionEnd) {
            return (
                <Off key="off" handleTransitionEnd={this.handleTransitionEnd} />
            )
        }
    },

    renderOn() {
        if (this.state.on && this.state.transitionEnd) {
            return (
                <On key="on" handleTransitionEnd={this.handleTransitionEnd} />
            )
        }
    },

    render: function() {
        return (
            <div>
              <button onClick={this.toggle}>Toggle</button>
              <ReactCSSTransitionGroup transitionName="switch">
                {this.renderOff()}
                {this.renderOn()}
              </ReactCSSTransitionGroup>
            </div>
        );         
    }
});

React.render(<Switch/>, document.getElementById("switch"));

And the relevant css:

.switch-enter {
    opacity: 0.01;
}
.switch-enter.switch-enter-active {
    opacity: 1.0;
    transition: opacity 500ms ease-in;
}
.switch-leave {
    opacity: 1.0;
}
.switch-leave.switch-leave-active {
    opacity: 0;
    transition: opacity 500ms ease-out;
}

You can achieve the same effective result with Jonny Buchanan's answer which uses absolute positioning and a delay instead of componentWillUnmount()

Mendive answered 1/11, 2015 at 20:8 Comment(2)
@JonnyBuchanan Both of your approaches seem useful. Now that some time has passed, which approach have you found works best for you? @RickJolly, does your approach work best for certain scenarios? Does it scale up to larger lists (children) within ReactCSSTransitionGroup well? The way I understand it is that your approach incurs an extra render() cycle (maybe an extra cycle for each item being removed?). Have you found that to have advantages or disadvantages? (Perhaps debugging - maybe its clearer what is going on with the extra render()?)Schwab
Also, what about the fact that the components to be animated must implement componentWillUnmount, as well as take the handleTransitionEnd() as a prop... have you found that to be a pain for other situations?Schwab
K
19

Another solution is to make the incoming and outgoing elements take up the same space, for example by having them both absolutely positioned:

<ReactCSSTransitionGroup
    className="container"
    component="div"
    transitionName="switch">
...

.container {
    position: relative;
}
.container > div {
    position: absolute;
}

http://jsfiddle.net/phepyezx/7/


You can use transition-delay to wait until the leaving component disappears before making the entering component appear, e.g.:

.fade-enter {
  opacity: 0.01;
}
.fade-enter.fade-enter-active {
  opacity: 1;
  transition: opacity 1s;
  transition-delay: 1s;
}

.fade-leave {
  opacity: 1;
}
.fade-leave.fade-leave-active {
  opacity: 0.01;
  transition: opacity 1s;
}
Kerby answered 30/10, 2015 at 2:58 Comment(7)
Yes. Not quite the same effect as completely fading out the original first though. I guess what I'm after isn't possible without trickery. I suppose I could use the lower level ReactTransisionGroup's componentDidLeave() lifecycle method to set state in order to know when begin to render the new component. Another approach is to use plain javascript to apply a transitionend event listener on the underlying DOM node as demonstrated in this post: chloechen.io/react-animation-done-in-two-waysMendive
Added a new bit about transition-delayKerby
Thanks a lot! Almost perfect, but requires absolute positioning. Unfortunately, I haven't found anything where the incoming component is not mounted (or display:'none') until the outgoing component leaves.Mendive
Any reason you're opposed to the absolute positioning? If you don't specify top/bottom/left/right it's going to default to it's static position (in other words, it won't move), it just won't take up any space.Interfaith
This works fantastically well, particularly when paired up with flexbox to size up the components being transitioned in and out. Using a parent container with position:relative and a child with position:absolute is far nicer than the hack approach I was previously using. The React tab/panel component I created works exactly as desired now, including the overlaid transition between panels.Irrelievable
Great solution, I was trying to position absolute/relative but was struggling adding to ReactCSSTransitionGroup and cloned elements this did the trick, thanks!Frausto
I'm working with gsap and I could just add this into the TweenMax options. Fantastic solution after struggling half a day on it.Trichome
M
12

Solved using the componentWillUnmount() lifecycle method.

http://jsfiddle.net/phepyezx/9/

Here's the code:

var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;

const Off = React.createClass({
    componentWillUnmount () {
        this.props.handleTransitionEnd();
    },
    render()  {
        return (
            <div className="off button">OFF</div>
        )
    }
});

const On = React.createClass({
    componentWillUnmount () {
        this.props.handleTransitionEnd();
    },
    render()  {
        return (
            <div className="on button">ON</div>
        )
    }
});

var Switch = React.createClass({
    getInitialState: function() {
        return {
            on: false,
            transitionEnd: true
        };
    },

    toggle: function(e) {
        this.setState({
            on: !this.state.on,
            transitionEnd: false
        });
    },

    handleTransitionEnd() {
        this.setState({transitionEnd: true});
    },

    renderOff() {
        if (! this.state.on && this.state.transitionEnd) {
            return (
                <Off key="off" handleTransitionEnd={this.handleTransitionEnd} />
            )
        }
    },

    renderOn() {
        if (this.state.on && this.state.transitionEnd) {
            return (
                <On key="on" handleTransitionEnd={this.handleTransitionEnd} />
            )
        }
    },

    render: function() {
        return (
            <div>
              <button onClick={this.toggle}>Toggle</button>
              <ReactCSSTransitionGroup transitionName="switch">
                {this.renderOff()}
                {this.renderOn()}
              </ReactCSSTransitionGroup>
            </div>
        );         
    }
});

React.render(<Switch/>, document.getElementById("switch"));

And the relevant css:

.switch-enter {
    opacity: 0.01;
}
.switch-enter.switch-enter-active {
    opacity: 1.0;
    transition: opacity 500ms ease-in;
}
.switch-leave {
    opacity: 1.0;
}
.switch-leave.switch-leave-active {
    opacity: 0;
    transition: opacity 500ms ease-out;
}

You can achieve the same effective result with Jonny Buchanan's answer which uses absolute positioning and a delay instead of componentWillUnmount()

Mendive answered 1/11, 2015 at 20:8 Comment(2)
@JonnyBuchanan Both of your approaches seem useful. Now that some time has passed, which approach have you found works best for you? @RickJolly, does your approach work best for certain scenarios? Does it scale up to larger lists (children) within ReactCSSTransitionGroup well? The way I understand it is that your approach incurs an extra render() cycle (maybe an extra cycle for each item being removed?). Have you found that to have advantages or disadvantages? (Perhaps debugging - maybe its clearer what is going on with the extra render()?)Schwab
Also, what about the fact that the components to be animated must implement componentWillUnmount, as well as take the handleTransitionEnd() as a prop... have you found that to be a pain for other situations?Schwab
F
2

If you want to delay the rendering of the next component, you could use something like this:

import React, { Component } from 'react';

export default class DelayedRender extends Component {

    static propTypes = {
        delay: React.PropTypes.number.isRequired,
        children: React.PropTypes.element,
        className: React.PropTypes.string
    };

    constructor(props) {
        super(props);

        this.state = {
            render: false
        };
    }

    componentDidMount() {
        setTimeout(() => {
            const delayedClassNames = this.refs.noDelayed.className;
            this.setState({
                render: true,
                classNames: delayedClassNames
            });
        }, this.props.delay);
    }

    render() {
        const { children, className } = this.props;
        return this.state.render ?
            <div className={this.state.classNames}>{children}</div> :
            <div className={className} ref="noDelayed" ></div>;
    }
}

And in your render method:

const ROUTE_TRANSITION_TIME = 500;
const views = [];

if (shouldRenderDelayedRoute) {
    views.push(
        <DelayedRender delay={ROUTE_TRANSITION_TIME} key="book">
            <A ref="book"/>
        </DelayedRender>
    );
} else {
    views.push(<B key="library"/>);
} 

<ReactCSSTransitionGroup
   transitionEnterTimeout={ROUTE_TRANSITION_TIME}
   transitionLeaveTimeout={ROUTE_TRANSITION_TIME}
   transitionName="fade-transition"
                        >
    {views}
</ReactCSSTransitionGroup>
Figurine answered 13/4, 2016 at 10:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.