reactjs - Using React TransitionGroup, GSAP, and MobX to Re-Render the Same Component -
i have survey-like react application rendering various questions screen using variety of ui components.
however, nature of survey many questions re-render using exact same component. instance, "a / b selector" or "checklist."
what i'd achieve each component, regardless of whether being re-used or mounted dom first time, fade bottom - , fade downward once user selects answer.
here basic example using 5 questions , little box:
import react, {component, proptypes} 'react'; import reactdom 'react-dom'; import { observer, provider, inject } 'mobx-react'; import { observable, computed, action } 'mobx'; import 'gsap'; import transitiongroup '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 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> ); } }
this works! but... pull off, have manually remove box component dom (in case via showingbox
observable), set timeout transition duration, , re-mount component completely.
ultimately guess fine wondering whether in community has come across similar scenario better way of addressing since unmount / remount isn't terribly strong react.
rodrigo's original answer
you try using <transitiongroup>
, <transition>
tag each component , on tag use onenter
, onexit
. that's 1 approach several components inside parent component:
<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>;
here's example using code, react router. not you're after working sample using approach. go components folder, routes.js
file:
https://codesandbox.io/s/mqy3mmznn
the caveat duration set in transition group config, should same of gsap instance in order keep mount/unmount in sync, since onenter
, onexit
don¡t provide callback.
another option use addendlistener
method of <transition>
element:
<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>
in case method provide done
callback can pass oncomplete
handler in angular. in mind, caveat duration in transition group config should longer time in gsap instance, otherwise component unmounted before animation complete. if longer doesn't matter, done callback unmounting you.
here's live sample, go components folder , children.js
file:
https://codesandbox.io/s/yvye9nnw
zfalen's derived solution
rodrigo's second suggestion, leveraging <transition />
component react-transition-group (note not same react-addons-transition-group
) lead me pretty ideal solution.
by using static value inside of mobx store, can declare single animation duration , derive everywhere else. furthermore, wrapping <transition />
higher-order component function lets me use decorator indicate animation given component should have!
as long pair animation mobx @inject()
/ <provider />
pattern, can declare gsap transitions separately, tag relevant components needed, , control store.
here's raw code sample (note need have node spun webpack / babel config supports decorators, etc. , little styling make stuff appear.):
import react, {component, proptypes} 'react'; import reactdom 'react-dom'; import { observer, provider, inject } 'mobx-react'; import { observable, computed, action } 'mobx'; require('../../../public/stylesheets/transpage.scss'); import 'gsap'; import transition "react-transition-group/transition"; // lil utility functions const tc = require('../../utils/timeconverter'); // mobx store class app_store { // toggle & duration transition @observable showingbox = true; transduration = .25; @observable questionindex = 0; @observable questions = [ {text: 0 }, ]; @computed currentquestion() { return this.questions[this.questionindex]; } @action testtrans () { // toggle component transition out this.showingbox = !this.showingbox; // wait until transition out completes // make changes affect state / props // transition component in settimeout(() => { // in case, add new 'question' 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 // tag components 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"> {/* dont need reference transition here */} {/* 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') ) }) }
Comments
Post a Comment