使用React TransitionGroup,GSAP和MobX重新渲染相同的组件

时间:2017-08-14 21:57:53

标签: reactjs greensock gsap mobx react-transition-group

我有一个类似调查的React应用程序,它使用各种UI组件向屏幕呈现各种问题。

然而,调查的性质是许多问题使用完全相同的组件重新渲染。例如,“A / B选择器”或“清单”。

我想要实现的是每个组件,无论是第一次重新使用还是安装到DOM,都从底部淡出 - 一旦用户选择答案就会向下淡化。

这是一个非常基本的例子,使用了五个问题和一个小方框:

import React, {Component, PropTypes} from 'react';
import ReactDOM from 'react-dom';

import { observer, Provider, inject } from 'mobx-react';
import { observable, computed, action } from 'mobx';

import 'gsap';
import TransitionGroup from 'react-addons-transition-group';

// STORE
class APP_STORE {

  @observable showingBox = true;
  @observable transDuration = .25;

  @observable questionIndex = 0;

  @observable questions = [
    {text: 'one'},
    {text: 'two'},
    {text: 'three'},
    {text: 'four'},
    {text: 'five'},
  ];

  @computed get currentQuestion() {
    return this.questions[this.questionIndex];
  }

  @action testTrans () {
    this.showingBox = !this.showingBox;
    setTimeout(() => {
      this.questionIndex++;
      this.showingBox = !this.showingBox;
    }, this.transDuration * 1000);
  }

}

let appStore = new APP_STORE();


// TRANSITION w/HIGHER ORDER COMPONENT
function fadesUpAndDown (Component) {
  return (
    class FadesUp extends React.Component {
      componentWillAppear (callback) {
        const el = ReactDOM.findDOMNode(this);
        TweenMax.fromTo(el, appStore.transDuration, {y: 100, opacity: 0}, {y: Math.random() * -100, opacity: 1, onComplete: callback});
      }

      componentWillEnter (callback) {
        const el = ReactDOM.findDOMNode(this);
        TweenMax.fromTo(el, appStore.transDuration, {y: 100, opacity: 0}, {y: Math.random() * -100, opacity: 1, onComplete: callback});
      }

      componentWillLeave (callback) {
        const el = ReactDOM.findDOMNode(this);
        TweenMax.to(el, appStore.transDuration, {y: 100, opacity: 0, onComplete: callback});
      }

      render () {
        return <Component ref="child" {...this.props} />;
      }
    }
  )
}

// REACT

@fadesUpAndDown
class Box extends React.Component {
  render () {
    return <div className="box" ref={c => this.container = c}> {this.props.data.text} </div>;
  }
}


@inject('store') @observer
class Page extends Component {

  render () {
    const store = this.props.store;

    return (
      <div className="page">
        <TransitionGroup>
          { store.showingBox ? <Box data={store.currentQuestion}/> : null }
        </TransitionGroup>

        <button className="toggle-btn" onClick={() => { store.testTrans(); } } >
          test trans
        </button>
      </div>
    );
  }

}

这个有效!但是...... 要把它拉下来,我必须手动从DOM 中移除盒子组件(在这种情况下通过showingBox observable),设置一个超时转换持续时间,并完全重新安装组件。

最终我觉得这很好,但我想知道SO社区中是否有人遇到类似的情况,更好的解决方法,因为卸载/重新安装并不是非常强大的React。

1 个答案:

答案 0 :(得分:3)

RODRIGO的原始答案

您可以尝试使用<TransitionGroup>,每个组件的<Transition>标记,并使用onEnteronExit标记。这是父组件内部几个组件的一种方法:

<TransitionGroup className="col-12">
  <Transition
    key={props.location.pathname}
    timeout={500}
    mountOnEnter={true}
    unmountOnExit={true}
    onEnter={node => {
      TweenLite.to(node, 0.5, {
        autoAlpha: 1,
        y: Math.random() * -100
      });
    }}
    onExit={node => {
      TweenLite.to(node, 0.5, {
        position: "fixed",
        autoAlpha: 1,
        y: 0
      });
    }}
  />
</TransitionGroup>;

以下是使用此代码的示例,但使用了react路由器。显然不是你所追求的,而是使用这种方法的工作样本。转到components文件夹,转到routes.js文件:

https://codesandbox.io/s/mQy3mMznn

唯一需要注意的是,转换组配置中设置的持续时间应该与GSAP实例相同,以便使挂载/卸载保持同步,因为onEnteronExit不是提供任何回调。

另一个选项是使用addEndListener元素的<Transition>方法:

<Transition
  in={this.props.in}
  timeout={duration}
  mountOnEnter={true}
  unmountOnExit={true}
  addEndListener={(n, done) => {
    if (this.props.in) {
      TweenLite.to(n, 1, {
        autoAlpha: 1,
        x: 0,
        ease: Back.easeOut,
        onComplete: done
      });
    } else {
      TweenLite.to(n, 1, { autoAlpha: 0, x: -100, onComplete: done });
    }
  }}
>
  {state =>
    <div className="card" style={{ marginTop: "10px", ...defaultStyle }}>
      <div className="card-block">
        <h1 className="text-center">FADE IN/OUT COMPONENT</h1>
      </div>
    </div>}
</Transition>

在这种情况下,该方法确实提供了done回调,您可以像Angular一样传递给onComplete处理程序。考虑到这一点,唯一需要注意的是转换组配置中的持续时间应该比GSAP实例中的时间长,否则在动画完成之前将卸载该组件。如果它更长并不重要,完成的回调会为你卸载。

这是一个实时示例,转到components文件夹并进入children.js文件:

https://codesandbox.io/s/yvYE9NNW

ZFALEN的衍生解决方案

Rodrigo的第二个建议,利用react-transition-group 中的<Transition />组件(请注意, react-addons-transition-group相同)最终引导我找到一个非常理想的解决方案

通过在我的MobX商店中使用静态值,我可以声明一个动画持续时间并在其他地方派生它。此外,将<Transition />包装为高阶组件函数让我只需使用装饰器来指示任何给定组件应具有的动画!

只要我将动画与MobX @inject() / <Provider />模式配对,我基本上可以单独声明我的GSAP过渡,根据需要标记相关组件,并控制商店中的所有内容。

这是一个原始代码示例(请注意,您需要使用支持装饰器等的Webpack / Babel配置启动Node,并且还要做一些样式以显示内容。)

import React, {Component, PropTypes} from 'react';
import ReactDOM from 'react-dom';

import { observer, Provider, inject } from 'mobx-react';
import { observable, computed, action } from 'mobx';

require('../../../public/stylesheets/transPage.scss');

import 'gsap';
import Transition from "react-transition-group/Transition";

// LIL UTILITY FUNCTIONS
const TC = require('../../utils/timeConverter');

// MOBX STORE
class APP_STORE {

  // A TOGGLE & DURATION FOR THE TRANSITION
  @observable showingBox = true;
  transDuration = .25;

  @observable questionIndex = 0;

  @observable questions = [
    {text: 0 },
  ];

  @computed get currentQuestion() {
    return this.questions[this.questionIndex];
  }

  @action testTrans () {

    // TOGGLE THE COMPONENT TO TRANSITION OUT
    this.showingBox = !this.showingBox;

    // WAIT UNTIL THE TRANSITION OUT COMPLETES
    // THEN MAKE CHANGES THAT AFFECT STATE / PROPS
    // THEN TRANSITION THE COMPONENT BACK IN
    setTimeout(() => {

      // IN THIS CASE, ADD A NEW 'QUESTION' TO THE SURVEY ARBITRARILY
      this.questions.push({text: this.questionIndex + 1 });
      this.questionIndex++;

      this.showingBox = !this.showingBox;
    }, TC.toMilliseconds(this.transDuration) );
  }

}

let appStore = new APP_STORE();


// TRANSITION w/HIGHER ORDER COMPONENT
function fadesUpAndDown (Component) {
  return (
    class FadesUp extends React.Component {

      constructor(props) {
        super(props);
      }

      render () {
        const store = this.props.store;

        return (
          <Transition
            in={store.showingBox}
            timeout={TC.toMilliseconds(store.transDuration)}
            mountOnEnter={true}
            unmountOnExit={true}
            addEndListener={(n, done) => {
              if (store.showingBox) {
                TweenLite.to(n, store.transDuration, {
                  opacity: 1,
                  y: -25,
                  ease: Back.easeOut,
                  onComplete: done
                });
              } else {
                TweenLite.to(n, store.transDuration, { opacity: 0, y: 100, onComplete: done });
              }
            }}
          >
            { state => <Component {...this.props} /> }
          </Transition>
        )
      }
    }
  )
}

// REACT STUFF

// JUST TAG COMPONENTS WITH THEIR RELEVANT TRANSITION
@inject("store") @observer @fadesUpAndDown
class Box extends React.Component {

  constructor(props) {
    super(props);
  }

  render () {
    return <div className="box" > {this.props.data.text} </div>
  }
}


@inject('store') @observer
class Page extends Component {

  render () {
    const store = this.props.store;

    return (
      <div className="page">
        {/* YOU DONT NEED TO EVEN REFERENCE THE TRANSITION HERE */}
        {/* IT JUST WORKS BASED ON DERIVED MOBX VALUES */}
        <Box data={store.currentQuestion} />

        <button className="toggle-btn" onClick={() => { store.testTrans(); } } >
          test transition
        </button>
      </div>
    );
  }

}


ReactDOM.render(
  <Provider store={appStore}>
    <Page />
  </Provider>,
  document.getElementById('renderDiv')
);

if (module.hot) {
  module.hot.accept( () => {
    ReactDOM.render(
      <Provider store={appStore}>
        <Page />
      </Provider>,
      document.getElementById('renderDiv')
    )
  })
}