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

Using React TransitionGroup, GSAP, and MobX to Re-Render the Same Component

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

但是,调查的本质是许多问题使用完全相同的组件重新呈现。例如,"A / B Selector" 或 "Checklist."

我想要实现的是每个组件,无论它是被重新使用还是第一次安装到 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。

罗德里戈的原始答案

您可以尝试使用 <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 路由器的示例。显然不是您想要的,而是使用这种方法的工作示例。转到组件文件夹,到 routes.js 文件:

https://codesandbox.io/s/mQy3mMznn

唯一需要注意的是,过渡组配置中设置的持续时间应该与 GSAP 实例相同,以保持 mount/unmount 同步,因为 onEnteronExit 不提供任何回调。


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

<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 实例中的时间,否则组件将在动画完成之前卸载。如果更长也没关系,done 回调会为您卸载。

这是一个实时示例,转到组件文件夹并进入 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')
    )
  })
}