React TransitionGroup and React.cloneElement do not send updated props
Asked Answered
R

1

19

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.cloneElementdoes 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 dispatched
    • componentWillMount
  • When the component's componentWillLeave or componentWillEnter hook is called, the corresponding slice of the options is sent to doTransition
  • In doTransition:
    • The user supplied transitionBegin function is called (optionSlice.transitionBegin)
    • The default action.willLeave or action.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

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?

enter image description here

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

On-Screen Transition: Transition

Transition in React DOM

Transition2

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!!

Rigger answered 31/12, 2016 at 0:3 Comment(0)
K
18

Determining Entering and Leaving Children

Imagine rendering the sample JSX below:

<TransitionGroup>
  <div key="one">Foo</div>
  <div key="two">Bar</div>
</TransitionGroup>

The <TransitionGroup>'s children prop would be made up of the elements:

[
  { type: 'div', props: { key: 'one', children: 'Foo' }},
  { type: 'div', props: { key: 'two', children: 'Bar' }}
]

The above elements will be stored as state.children. Then, we update the <TransitionGroup> to:

<TransitionGroup>
  <div key="two">Bar</div>
  <div key="three">Baz</div>
</TransitionGroup>

When componentWillReceiveProps is called, its nextProps.children will be:

[
  { type: 'div', props: { key: 'two', children: 'Bar' }},
  { type: 'div', props: { key: 'three', children: 'Baz' }}
]

Comparing state.children and nextProps.children, we can determine that:

1. { type: 'div', props: { key: 'one', children: 'Foo' }} is leaving

2. { type: 'div', props: { key: 'three', children: 'Baz' }} is entering.

In a regular React application, this means that <div>Foo</div> would no longer be rendered, but that is not the case for the children of a <TransitionGroup>.

How <TransitionGroup> Works

So how exactly is <TransitionGroup> able to continue rendering components that no longer exist in props.children?

What <TransitionGroup> does is that it maintains a children array in its state. Whenever the <TransitionGroup> receives new props, this array is updated by merging the current state.children and the nextProps.children. (The initial array is created in the constructor using the initial children prop).

Now, when the <TransitionGroup> renders, it renders every child in the state.children array. After it has rendered, it calls performEnter and performLeave on any entering or leaving children. This in turn will perform the transitioning methods of the components.

After a leaving component's componentWillLeave method (if it has one) has finished executing, it will remove itself from the state.children array so that it no longer renders (assuming it didn't re-enter while it was leaving).

Passing Props to Leaving Children?

Now the question is, why aren't updated props being passed to the leaving element? Well, how would it receive props? Props are passed from a parent component to a child component. If you look at the example JSX above, you can see that the leaving element is in a detached state. It has no parent and it is only rendered because the <TransitionGroup> is storing it in its state.

When you are attempting to inject the state to the children of your <TransitionGroup> through React.cloneElement, the leaving component is not one of those children.

The Good News

You can pass a childFactory prop to your <TransitionGroup>. The default childFactory just returns the child, but you can take a look at the <CSSTransitionGroup> for a more advanced child factory.

You can inject the correct props into the children (even the leaving ones) through this child wrapper.

function childFactory(child) {
  return React.cloneElement(child, {
    transitionState,
    dispatch
  })
}

Usage:

var ConnectedTransitionGroup = connect(
  store => ({
   transitionState: state.transitions
  }),
  dispatch => ({ dispatch })
)(TransitionGroup)

render() {
  return (
    <ConnectedTransitionGroup childFactory={childFactory}>
      {children}
    </ConnectedTransitionGroup>
  )
}

React Transition Group was somewhat recently split out of the main React repo and you can view its source code here. It is pretty straightforward to read through.

Kilogrammeter answered 31/12, 2016 at 6:3 Comment(9)
Since I need to access the state in the Transition HOC, I would naturally wrap it in connect(). However, the problem then is that TransitionGroup will stop working, as the connect-ed Transition HOC's now do not implement the componentWillLeave and componentWillEnter hooks. Or am I not seeing something? Thanks for all the help by the way @Paul S.Rigger
That does complicate things. What are the transition states that you are sending to your transitioning components?Kilogrammeter
Its just which elements are leaving/entering. Something like {home: "willLeave", bom: "willEnter", otherPage1:"transitionComplete" The reason I do this is so that I can dynamically change the transition based on which route is being taken. @PaulSRigger
Is that related to your optionSlice.transitionBegin function? I don't think that I am fully understanding the need here tracking which components are transitioing in Redux. Can the transition for one component vary based on which other components are also transitioning?Kilogrammeter
No, and yes. optionSlice.transitionBegin and optionSlice.transitionEndis just an optional function that can be passed in to be executed during the animation. But you are correct in that the reason im tracking the transition components is that the transition can change based on which component is entering. So really, all I need to know is which component[s] are entering. Please see my edit. I hope that will bring some clarity. @Paul S.Rigger
Edited my post with slightly more detail (see the "The Good News" section), but I think that you should pass a childFactory prop to your transition group to inject props into all of the children.Kilogrammeter
Key is not being sent for some reason, and the componentWillEnter and componentWillLeave hooks are not being called (and the animation is not being executed). Please see my update @Paul S.Rigger
Thank you for explaining the inner workings of TransitionGroup so thoroughly @Paul S. I am new to React and the documentation on React Transition Group is very brief so I've been struggling to get my head around it. I have a question about cancelling the transition effect of a leaving/entering child. I'm not sure if childFactory would be useful in this case but I was wondering if you could please take a look at the question here? I have tried using the onEnter callback and on enter prop to no avail.Agar
I've tried using childFactory in my example and it's almost yielding the result that I am after except that it does not update the timeout of all leaving children. To illustrate, when you click Next on app launch, the transition from child First to child Second begins. If you click Next again before the transition has ended, the timeout of child First does not update. I'm not sure I understand why.Agar

© 2022 - 2024 — McMap. All rights reserved.