React TransitionGroup 和 React.cloneElement 不发送更新的道具

React TransitionGroup and React.cloneElement do not send updated props

我正在学习 Chang Wang 关于使用 HOC 和 ReactTransitionGroup(Part 1 Part 2) in conjunction with Huan Ji's tutorial on page transitions (Link).

制作可重用 React 转换的教程

我面临的问题是 React.cloneElement 似乎没有将更新的道具传递到其 children 之一,而其他 children 确实正确接收更新的道具。

首先,一些代码:

TransitionContainer.js

TransitionContainer是一个容器组件,类似于环霁教程中的App。它向 children.

注入状态的一部分

TransitionGroup 的 children 都是一个名为 Transition 的 HOC 实例(代码更往下)

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 类似于 Chang Wang 的 HOC。它需要一些选项,定义 componentWillEnter + componentWillLeave 钩子,并包装一个组件。 TransitionContainer(上图)将 props.transitionState 注入到这个 HOC 中。但是,有时即使状态发生变化,道具也不会更新(请参阅下面的问题

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}/>
    }
  }
}

选项

选项具有以下结构:

{
  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.}

  }
}

过渡生命周期(onEnter 或 onLeave)

optionSlice本质上只是允许用户传入一些选项。 optionSlice.transitionBeginoptionSlice.transitionEnd 只是可选函数,如果适合用例,则在动画进行时执行。我目前没有为我的组件传递任何东西,但我希望尽快把它变成一个库,所以我只是覆盖我的基础。

为什么我还要跟踪过渡状态?

根据进入的元素,退出动画会发生变化,反之亦然。

比如上图中,蓝色进入时红色向右移动,蓝色退出时红色向左移动。但是,当绿色进入时,红色向左移动,而当绿色退出时,红色向右移动。为了控制这一点,我需要知道当前转换的状态。

问题:

TransitionGroup包含两个元素,一进一出(受react-router控制)。它将一个名为 transitionState 的道具传递给它的 children。 Transition HOC(TransitionGroup 的 children)通过动画过程调度某些 redux 动作。正在进入的 Transition 组件按预期接收道具更改,但正在退出的组件被冻结。它的道具不会改变。

总是退出的那个没有收到更新的道具。我试过切换包装组件(退出和进入),问题不是由于包装组件。

图片

On-Screen 过渡:

React 中的转换 DOM

现有组件 Transition(Connect(Home))),在这种情况下,没有接收更新的道具。

知道为什么会这样吗?预先感谢所有帮助。

更新 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)

我已将 TransitionContainer 更新为以上内容。现在,componentWillEntercomponentWillLeave 钩子没有被调用。我在 childFactory 函数中记录了 React.cloneElement(child, {...}),挂钩(以及我定义的函数,如 doTransition)出现在 prototype 属性中。仅调用 constructorcomponentWillMountcomponentWillUnmount。我怀疑这是因为 key 道具没有通过 React.cloneElement 注入。 transitionStatedispatch 正在被注入。

更新二:

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)

进一步检查TransitionGroup源码后,我意识到我把密钥放错了地方。现在一切都很好。非常感谢您的帮助!!

确定进入和离开Children

想象一下渲染下面的示例 JSX:

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

<TransitionGroup>children 道具将由以下元素组成:

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

以上元素将存储为state.children。然后,我们将 <TransitionGroup> 更新为:

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

调用componentWillReceiveProps时,其nextProps.children将是:

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

比较state.childrennextProps.children,我们可以确定:

1。 { type: 'div', props: { key: 'one', children: 'Foo' }} 即将离开

2。 { type: 'div', props: { key: 'three', children: 'Baz' }} 正在进入。

在常规的 React 应用程序中,这意味着 <div>Foo</div> 将不再被渲染,但 children 的 <TransitionGroup>.[=55 的情况并非如此=]

<TransitionGroup> 的工作原理

那么 <TransitionGroup> 究竟是如何能够继续渲染 props.children 中不再存在的组件的呢?

<TransitionGroup> 的作用是维护一个 children 数组的状态。每当 <TransitionGroup> 接收到新道具时,此数组将由 merging 当前 state.childrennextProps.children 更新。 (初始数组是使用初始 children 属性在 constructor 中创建的)。

现在,当 <TransitionGroup> 呈现时,它会呈现 state.children 数组中的每个 child。渲染完成后,它会在任何进入或离开 children 时调用 performEnterperformLeave。这反过来将执行组件的转换方法。

在离开组件的 componentWillLeave 方法(如果有的话)执行完毕后,它将自己从 state.children 数组中移除,这样它就不再呈现(假设它没有 re-enter 在它离开的时候)。

将道具传递给离开 Children?

现在的问题是,为什么不将更新的属性传递给离开元素?那么,它将如何接收道具呢?道具从 parent 组件传递到 child 组件。如果您查看上面的示例 JSX,您可以看到离开元素处于分离状态。它没有 parent 并且它被渲染只是因为 <TransitionGroup> 将它存储在它的 state 中。

当您尝试通过 React.cloneElement 将状态注入 <TransitionGroup> 的 children 时,离开组件不是 children 中的一个。

好消息

您可以将 childFactory 道具传递给您的 <TransitionGroup>。默认 childFactory 只是 returns child,但您可以查看 <CSSTransitionGroup> 以获得更多 advanced child factory

您可以通过这个 child 包装器将正确的 props 注入 children(甚至剩下的)。

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

用法:

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

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

React Transition Group 最近从主要的 React 存储库中分离出来,您可以查看其源代码 here。通读起来非常简单。