I am following Chang Wang's tutorial for making reusable React transitions with HOCs and ReactTransitionGroup
(Part 1 Part 2) in conjunction with Huan Ji's tutorial on page transitions (Link).
The problem I am facing is that React.cloneElement
does not seem to be passing updated props into one of its children, while other children do properly receive updated props.
First, some code:
TransitionContainer.js
TransitionContainer
is a container component that is akin to App
in Huan Ji's tutorial. It injects a slice of the state to it's children.
The children of the TransitionGroup
are all an instance of an HOC called Transition
(code further down)
import React from 'react';
import TransitionGroup from 'react-addons-transition-group';
import {connect} from 'react-redux';
class TransitionContainer extends React.Component{
render(){
console.log(this.props.transitionState);
console.log("transitionContainer");
return(
<div>
<TransitionGroup>
{
React.Children.map(this.props.children,
(child) => React.cloneElement(child, //These children are all instances of the Transition HOC
{ key: child.props.route.path + "//" + child.type.displayName,
dispatch: this.props.dispatch,
transitionState: this.props.transitionState
}
)
)
}
</TransitionGroup>
</div>
)
}
}
export default connect((state)=>({transitionState:state.transitions}),(dispatch)=>({dispatch:dispatch}))(TransitionContainer)
Transition.js
Transition
is akin to Chang Wang's HOC. It takes some options, defines the componentWillEnter
+ componentWillLeave
hooks, and wraps a component. TransitionContainer
(above) injects props.transitionState
into this HOC. However, sometimes the props do not update even if state changes (see The Problem below)
import React from 'react';
import getDisplayName from 'react-display-name';
import merge from 'lodash/merge'
import classnames from 'classnames'
import * as actions from './actions/transitions'
export function transition(WrappedComponent, options) {
return class Transition extends React.Component {
static displayName = `Transition(${getDisplayName(WrappedComponent)})`;
constructor(props) {
super(props);
this.state = {
willLeave:false,
willEnter:false,
key: options.key
};
}
componentWillMount(){
this.props.dispatch(actions.registerComponent(this.state.key))
}
componentWillUnmount(){
this.props.dispatch(actions.destroyComponent(this.state.key))
}
resetState(){
this.setState(merge(this.state,{
willLeave: false,
willEnter: false
}));
}
doTransition(callback,optionSlice,willLeave,willEnter){
let {transitionState,dispatch} = this.props;
if(optionSlice.transitionBegin){
optionSlice.transitionBegin(transitionState,dispatch)
}
if(willLeave){
dispatch(actions.willLeave(this.state.key))
}
else if(willEnter){
dispatch(actions.willEnter(this.state.key))
}
this.setState(merge(this.state,{
willLeave: willLeave,
willEnter: willEnter
}));
setTimeout(()=>{
if(optionSlice.transitionComplete){
optionSlice.transitionEnd(transitionState,dispatch);
}
dispatch(actions.transitionComplete(this.state.key))
this.resetState();
callback();
},optionSlice.duration);
}
componentWillLeave(callback){
this.doTransition(callback,options.willLeave,true,false)
}
componentWillEnter(callback){
this.doTransition(callback,options.willEnter,false,true)
}
render() {
console.log(this.props.transitionState);
console.log(this.state.key);
var willEnterClasses = options.willEnter.classNames
var willLeaveClasses = options.willLeave.classNames
var classes = classnames(
{[willEnterClasses] : this.state.willEnter},
{[willLeaveClasses] : this.state.willLeave},
)
return <WrappedComponent animationClasses={classes} {...this.props}/>
}
}
}
options
Options have the following structure:
{
willEnter:{
classNames : "a b c",
duration: 1000,
transitionBegin: (state,dispatch) => {//some custom logic.},
transitionEnd: (state,dispatch) => {//some custom logic.}
// I currently am not passing anything here, but I hope to make this a library
// and am adding the feature to cover any use case that may require it.
},
willLeave:{
classNames : "a b c",
duration: 1000,
transitionBegin: (state,dispatch) => {//some custom logic.},
transitionEnd: (state,dispatch) => {//some custom logic.}
}
}
Transition Lifecycle (onEnter or onLeave)
- When the component is mounted,
actions.registerComponent
is dispatchedcomponentWillMount
- When the component's
componentWillLeave
orcomponentWillEnter
hook is called, the corresponding slice of the options is sent todoTransition
- In doTransition:
- The user supplied transitionBegin function is called (
optionSlice.transitionBegin
) - The default
action.willLeave
oraction.willEnter
is dispatched - A timeout is set for the duration of the animation (
optionSlice.duration
). When the timeout is complete:- The user supplied transitionEnd function is called (
optionSlice.transitionEnd
) - The default
actions.transitionComplete
is dispatched
- The user supplied transitionEnd function is called (
- The user supplied transitionBegin function is called (
Essentially, optionSlice just allows the user to pass in some options. optionSlice.transitionBegin
and optionSlice.transitionEnd
are just optional functions that are executed while the animation is going, if that suits a use case. I'm not passing anything in currently for my components, but I hope to make this a library soon, so I'm just covering my bases.
Why Am I tracking transition states anyway?
Depending on the element that is entering, the exiting animation changes, and vice versa.
For example, in the image above, when the blue enters, red moves right, and when the blue exits, red moves left. However when the green enters, red moves left and when the green exits, red moves right. To control this is why I need to know the state of current transitions.
The Problem:
The TransitionGroup
contains two elements, one entering, one exiting (controlled by react-router). It passes a prop called transitionState
to its children. The Transition
HOC (children of TransitionGroup
) dispatches certain redux actions through the course of an animation. The Transition
component that is entering receives the props change as expected, but the component that is exiting is frozen. It's props do not change.
It is always the one that is exiting that does not receive updated props. I have tried switching the wrapped components (exiting and entering), and the issues is not due to the wrapped components.
Images
Transition in React DOM
The exiting component Transition(Connect(Home))), in this case, is not receiving updated props.
Any ideas why this is the case? Thanks in advance for all the help.
Update 1:
import React from 'react';
import TransitionGroup from 'react-addons-transition-group';
import {connect} from 'react-redux';
var childFactoryMaker = (transitionState,dispatch) => (child) => {
console.log(child)
return React.cloneElement(child, {
key: (child.props.route.path + "//" + child.type.displayName),
transitionState: transitionState,
dispatch: dispatch
})
}
class TransitionContainer extends React.Component{
render(){
let{
transitionState,
dispatch,
children
} = this.props
return(
<div>
<TransitionGroup childFactory={childFactoryMaker(transitionState,dispatch)}>
{
children
}
</TransitionGroup>
</div>
)
}
}
export default connect((state)=>({transitionState:state.transitions}),(dispatch)=>({dispatch:dispatch}))(TransitionContainer)
I've updated my TransitionContainer
to the above. Now, the componentWillEnter
and componentWillLeave
hooks are not being called. I logged the React.cloneElement(child, {...})
in the childFactory
function, and the hooks (as well as my defined functions like doTransition
) are present in the prototype
attribute. Only constructor
, componentWillMount
and componentWillUnmount
are called. I suspect this is because the key
prop is not being injected through React.cloneElement
. transitionState
and dispatch
are being injected though.
Update 2:
import React from 'react';
import TransitionGroup from 'react-addons-transition-group';
import {connect} from 'react-redux';
var childFactoryMaker = (transitionState,dispatch) => (child) => {
console.log(React.cloneElement(child, {
transitionState: transitionState,
dispatch: dispatch
}));
return React.cloneElement(child, {
key: (child.props.route.path + "//" + child.type.displayName),
transitionState: transitionState,
dispatch: dispatch
})
}
class TransitionContainer extends React.Component{
render(){
let{
transitionState,
dispatch,
children
} = this.props
return(
<div>
<TransitionGroup childFactory={childFactoryMaker(transitionState,dispatch)}>
{
React.Children.map(this.props.children,
(child) => React.cloneElement(child, //These children are all instances of the Transition HOC
{ key: child.props.route.path + "//" + child.type.displayName}
)
)
}
</TransitionGroup>
</div>
)
}
}
export default connect((state)=>({transitionState:state.transitions}),(dispatch)=>({dispatch:dispatch}))(TransitionContainer)
After further inspection of the TransitionGroup source, I realized that I put the key in the wrong place. All is well now. Thanks so much for the help!!
Transition
HOC, I would naturally wrap it in connect(). However, the problem then is thatTransitionGroup
will stop working, as the connect-ed Transition HOC's now do not implement thecomponentWillLeave
andcomponentWillEnter
hooks. Or am I not seeing something? Thanks for all the help by the way @Paul S. – Rigger