简体   繁体   中英

React JS — TransitionGroup and Higher-order Components

I'm trying to animate a custom Modal component using TransitionGroup and TweenMax . To understand the structure of the Modal, please consider the following hypothetical, psuedo structure:

<Container>
   <Content>
     // modal contents in here...
   </Content>
   <Overlay/>
</Container>

I want <Container> to fade in and <Content> to fade in from the bottom. Here is a visual to help you understand; the red represents the overlay and the white represents the inner contents: 在此处输入图片说明

I actually got the above visual to work using the following code:

import React, { Component } from 'react';
import TransitionGroup from 'react-transition-group/TransitionGroup'; // npm install react-transition-group --save
import { TweenMax } from 'gsap'; // npm install gsap --save

class ParentChild extends React.Component {

    componentWillEnter(callback) {
        TweenMax.fromTo(this.parent, 0.33, {opacity: 0}, {opacity: 1, onComplete: callback});
        TweenMax.fromTo(this.child, 0.33, {opacity: 0, y: 100}, {opacity: 1, y: 0, onComplete: callback});
    }

    componentWillLeave(callback) {
        TweenMax.fromTo(this.parent, 0.33, {opacity: 1}, {opacity: 0, onComplete: callback});
        TweenMax.fromTo(this.child, 0.33, {opacity: 1, y: 0}, {opacity: 0, y: 100, onComplete: callback});
    }

    render() {
        const { size, children } = this.props;
        const parentStyle = {
            width: `${size}px`,
            height: `${size}px`,
            background: '#df4747',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center'
        };
        const childStyle = {
            background: '#fff',
            textAlign: 'center',
            color: '#000',
            width: '90%'
        }
        return(
            <div ref={el => this.parent = el} style={parentStyle}>
                <div ref={el => this.child = el} style={childStyle}>
                    {this.props.children}
                </div>
            </div>
        );
    }
}

class App extends Component {
    constructor() {
        super();
        this.state = {
            isVisible: false
        };
        this.handleToggle = this.handleToggle.bind(this);
        this.handleClose = this.handleClose.bind(this);
    }

    handleToggle() {
        this.setState({ isVisible: !this.state.isVisible });
    }

    handleClose() {
        this.setState({ isVisible: false });
    }

    render() {
        const { isVisible } = this.state;
        return(
            <div>
                <TransitionGroup>
                    {isVisible ?
                        <ParentChild size="150">
                            <p>I wanna fade up!</p>
                        </ParentChild>
                    : null}
                </TransitionGroup>

                <button onClick={this.handleToggle}>Toggle Visibility</button>
            </div>
        );
    }
}

export default App;

I want to make the above code more maintainable with the help of higher-order components, so I attempted the following. In my opinion, the following code is more maintainable because the TweenMax stuff is isolated into individual components (fadeInHOC and fadeInUpHOC), therefore, it can be reused on any type of React component. Additionally, if I ever wanted to change the animations, all I would have to do is create another HOC and change the wrapper function.

import React, { Component } from 'react';
import TransitionGroup from 'react-transition-group/TransitionGroup'; // npm install react-transition-group --save
import { TweenMax } from 'gsap'; // npm install gsap --save

const fadeInHOC = (Component) => {
    return class extends React.Component {

        componentWillEnter(callback) {
            TweenMax.fromTo(this.container, 0.33, {opacity: 0}, {opacity: 1, onComplete: callback});
        }

        componentWillLeave(callback) {
            TweenMax.fromTo(this.container, 0.33, {opacity: 1}, {opacity: 0, onComplete: callback});
        }

        render() {
            return <Component containerRef={el => this.container = el} {...this.props}/>;
        }
    }
}

const fadeInUpHOC = (Component) => {
    return class extends React.Component {

        componentWillEnter(callback) {
            TweenMax.fromTo(this.container, 0.33, {opacity: 0, y: 100}, {opacity: 1, y: 0, onComplete: callback});
        }

        componentWillLeave(callback) {
            TweenMax.fromTo(this.container, 0.33, {opacity: 1, y: 0}, {opacity: 0, y: 100, onComplete: callback});
        }

        render() {
            return <Component containerRef={el => this.container = el} {...this.props}/>;
        }
    }
}

const Parent = fadeInHOC((props) => {
    const size = props.size;
    const style = {
        width: `${size}px`,
        height: `${size}px`,
        background: '#df4747',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center'
    }
    return(
        <div ref={props.containerRef} style={style}>
            {props.children}
        </div>
    );
});

const Child = fadeInUpHOC((props) => {
    const style = {
        background: '#fff',
        textAlign: 'center',
        color: '#000',
        width: '90%'
    }
    return(
        <div ref={props.containerRef} style={style}>
            {props.children}
        </div>
    );
});

class App extends Component {
    constructor() {
        super();
        this.state = {
            isVisible: false
        };
        this.handleToggle = this.handleToggle.bind(this);
        this.handleClose = this.handleClose.bind(this);
    }

    handleToggle() {
        this.setState({ isVisible: !this.state.isVisible });
    }

    handleClose() {
        this.setState({ isVisible: false });
    }

    render() {
        const { isVisible } = this.state;
        return(
            <div>
                <TransitionGroup>
                    {isVisible ?
                        <Parent size="150">
                            <Child>
                                <p>I wanna fade up!</p>
                            </Child>
                        </Parent>
                    : null}
                </TransitionGroup>

                <button onClick={this.handleToggle}>Toggle Visibility</button>
            </div>
        );
    }
}

export default App;

The problem with the above code is that fadeInUp on <Child> is not triggered, only the outer animation on <Parent> works. Can you tell me how I can get it to work as intended? Should I use a different approach? Should I stick with the ParentChild approach without using higher-order components? I really appreciate your input. Thank you!

if you further wrapped ParentChild in a HOC component, the transition group will not work, we also met the same problem by hooking TransitionGroup with react connected component, the life cycle methods will not be triggered at all.

One way to do it is wrap ur Child component first with TransitionGroup like:

const WrappedChild = (props) =>{
  return <TransitionGroup>
  <Child {...props}/>
  </TransitionGroup>
}

and then export WrappedChild and make it as a child of Parent , all the life cycle methods will be available, in ur Child component. This will work fine in ur cases.

However, It will not work if your Parent contains multiple children, say, rendering by data.map((item)=> <Child ... />) and Child itself is a connected redux component (our situation).

Another way to do it is to use react-move ( https://github.com/react-tools/react-move ) instead of transitionGroup , which is the solution for our problem. It is very handy and you can custom the duration, ease curve, define different animation on different style in a single transition.

Then you do not need the TweenMax to handle the animation. Hopefully it will help you.

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