将组件用作 prop 时,React 上下文不会传输

React context doesn't transfer when using a component as prop

我正在使用 react-bootstrap 的 ModalTrigger 来显示一个字段密集的模态(基于 react-bootstrap 的模态),这意味着向它发送一堆道具:

<ModalTrigger modal={<MyModal field1={value1} field2={value2} (more fields...)/>}>
    Click here to open
</ModalTrigger>

创建触发器的父组件通过 props 传入 fields/values,that 组件的父组件也将其作为 props 传递,通过实际保存数据的顶级组件。两者基本上都是管道,这是一个经典的 childContext 场景,只是它不起作用。这是我尝试过的简化版本:

var MyModal = React.createClass({
    contextTypes : {foo : React.PropTypes.string},
    render : function() {
        return (
            <Modal {...this.props} title="MyTitle">
                <div className="modal-body">
                    The context is {this.context.foo}
                </div>
            </Modal>
        );
    }
});

var Content = React.createClass({
    childContextTypes : {foo: React.PropTypes.string},
    getChildContext : function() {return {foo : "bar"}},
    render : function() {
        return (
            <ModalTrigger modal={<MyModal/>}>
                <span>Show modal</span>
            </ModalTrigger>
        )
    }
});

模态弹出 "The context is",但不显示实际上下文。

我相信这是因为发送到 ModalTrigger 的道具已经 rendered/mounted 不知何故,但我不确定为什么。据我了解,MyModal 的所有者是 Content 组件,这意味着上下文应该没问题,但事实并非如此。

更多信息:我已经尝试将 {...this.props}context={this.context} 传递给 MyModal,但没有成功。此外,可能相关的是,ModalTrigger 使用 cloneElement 来确保模态的 onRequestHide 道具指向触发器的隐藏功能。

那么我在这里缺少什么? :/

React.cloneElement 将在覆盖 ref 属性时更改元素的所有者,这意味着上下文不会从先前的所有者传递。 然而,ModalTrigger似乎并非如此。

请注意,基于所有者的方法在 React 0.14 中将不再适用,因为上下文将从父级传递给子级,而不是从所有者传递给所有者。 ModalTrigger 在 DOM 的另一个分支中呈现其 modal 节点属性(参见 OverlayMixin)。因此,您的 Modal 组件既不是子组件也不是 Content 组件的后代,因此不会从 Content.

传递子上下文

至于解决您的问题,您始终可以创建一个组件,其唯一目的是将上下文传递给它的子组件。

var PassContext = React.createClass({
  childContextTypes: {
    foo: React.PropTypes.string
  },

  getChildContext: function() {
    return this.props.context;
  },

  render: function() {
    return <MyModal />;
  },
});

使用方法:

<ModalTrigger modal={<PassContext context={this.getChildContext()}/>}>

正如 Matt Smith 暗示的那样,事实证明 react-bootstrap 已经包含了一种非常相似的方法来通过 ModalTrigger.withContext 转发上下文。这允许您创建一个 ModalTrigger 组件 class,无论它在 VDOM 树中的位置如何,它都会将其上下文转发到其 modal 节点属性。

// MyModalTrigger.js
module.exports = ModalTrigger.withContext({
  foo: React.PropTypes.String
});

有一种更好的方法将上下文传递给 "portal" 类型的组件,这些组件将 children 渲染到 React 树之外的不同容器中。

使用 "renderSubtreeIntoContainer" 而不是 "render" 也会将上下文传递到子树中。

可以这样使用:

import React, {PropTypes} from 'react';
import {
    unstable_renderSubtreeIntoContainer as renderSubtreeIntoContainer,
    unmountComponentAtNode
} from 'react-dom';

export default class extends React.Component {
  static displayName = 'ReactPortal';

  static propTypes = {
    isRendered: PropTypes.bool,
    children: PropTypes.node,
    portalContainer: PropTypes.node
  };

  static defaultProps = {
    isRendered: true
  };

  state = {
    mountNode: null
  };

  componentDidMount() {
    if (this.props.isRendered) {
      this._renderPortal();
    }
  }

  componentDidUpdate(prevProps) {
    if (prevProps.isRendered && !this.props.isRendered ||
      (prevProps.portalContainer !== this.props.portalContainer &&
         prevProps.isRendered)) {
      this._unrenderPortal();
    }

    if (this.props.isRendered) {
      this._renderPortal();
    }
  }

  componentWillUnmount() {
    this._unrenderPortal();
  }

  _getMountNode = () => {
    if (!this.state.mountNode) {
      const portalContainer = this.props.portalContainer || document.body;
      const mountNode = document.createElement('div');
      portalContainer.appendChild(mountNode);
      this.setState({
        mountNode
      });

      return mountNode;
    }

    return this.state.mountNode;
  };

  _renderPortal = () => {
    const mountNode = this._getMountNode();
    renderSubtreeIntoContainer(
      this,
      (
        <div>
          {this.props.children}
        </div>
      ),
      mountNode,
    );
  };

  _unrenderPortal = () => {
    if (this.state.mountNode) {
      unmountComponentAtNode(this.state.mountNode);
      this.state.mountNode.parentElement.removeChild(this.state.mountNode);
      this.setState({
        mountNode: null
      });
    }
  };

  render() {
    return null;
  }
};

这是我在 production app Casalova 中使用的一个门户示例,它可以将上下文正确地呈现到他们的 children。

注意:此 API 未记录在案,将来可能会更改。不过现在,这是将上下文呈现到门户组件中的正确方法。