【问题标题】:Using React TransitionGroup, GSAP, and MobX to Re-Render the Same Component使用 React TransitionGroup、GSAP 和 MobX 重新渲染相同的组件
【发布时间】:2018-01-22 19:56:07
【问题描述】:

我有一个类似调查的 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 中删除 box 组件(在这种情况下通过 showingBox observable),设置过渡持续时间的超时, 并重新完全安装组件。

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

【问题讨论】:

    标签: reactjs gsap mobx react-transition-group


    【解决方案1】:

    罗德里戈的原始答案

    您可以尝试使用&lt;TransitionGroup&gt;,为每个组件使用&lt;Transition&gt; 标签,并在该标签上使用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 不提供任何回调.


    另一个选项是使用&lt;Transition&gt; 元素的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 回调会为您卸载。

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

    https://codesandbox.io/s/yvYE9NNW


    ZFALEN 的衍生解决方案

    Rodrigo 的第二个建议,利用 react-transition-group 中的 &lt;Transition /&gt; 组件(注意,这react-addons-transition-group 相同)理想的解决方案。

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

    只要我将动画与 MobX @inject() / &lt;Provider /&gt; 模式配对,我基本上可以单独声明我的 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')
        )
      })
    }
    

    【讨论】:

    • 谢谢,@rodrigo - 第二个例子与我要找的很接近。事实上,我在上面写它的方式非常相似,使用顶级isShowing 状态来强制挂载/卸载,而不管过渡组件是否只是接收新的道具。我会尝试并回复你
    • 你就是那个男人。第二种模式奏效了,我能够将它与 MobX / Decorators 很好地融合在一起。请参阅我附加到您的答案的编辑。
    • 很高兴为您提供帮助;)。也感谢您添加您的解决方案。 MobX 在这里不是一个常见的问题,所以我相信很多人都会从你的解决方案中受益。干杯!!!
    • 希望如此!刚刚在整个调查中实施了它,如果您只是将动画导出到他们自己的文件中,那就再好不过了,非常干燥和可维护。很酷的图案和图书馆!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-05-17
    • 2020-05-22
    • 1970-01-01
    • 1970-01-01
    • 2018-01-28
    • 1970-01-01
    • 2016-11-04
    相关资源
    最近更新 更多