在React中使用新的门户功能

来自菜鸟教程
跳转至:导航、​搜索

React v16 引入了一个名为 portals 的新功能。 该文件指出:

门户提供了一种一流的方式来将子级呈现到存在于父组件的 DOM 层次结构之外的 DOM 节点中。

通常,函数式或类组件会渲染一棵 React 元素树(通常从 JSX 生成)。 React 元素定义了父组件的 DOM 的外观。

在 v16 之前,只允许渲染少数子类型:

  • nullfalse(表示不渲染)。
  • JSX。
  • 反应元素。
function Example(props) {
  return null;
}
function Example(props) {
  return false;
}
function Example(props) {
  return <p>Some JSX</p>;
}
function Example(props) {
  return React.createElement(
    'p',
    null,
    'Hand coded'
  );
}

在 v16 中,更多的子类型变成了可渲染的:

  • 数字(包括 InfinityNaN)。
  • 字符串。
  • 反应门户。
  • 一组可渲染的孩子。

可渲染子级的完整列表

function Example(props) {
  return 42;  // Becomes a text node.
}
function Example(props) {
  return 'The meaning of life.';  // Becomes a text node.
}
function Example(props) {
  return ReactDOM.createPortal(
    // Any valid React child type
    [
      'A string',
      <p>Some JSX</p>,
      'etc'
    ],
    props.someDomNode
  );
}

React 门户是通过调用 ReactDOM.createPortal 创建的。 第一个参数应该是一个可渲染的孩子。 第二个参数应该是对渲染可渲染子节点的 DOM 节点的引用。 ReactDOM.createPortal 返回一个本质上类似于 React.createElement 返回的对象。

请注意,createPortal 位于 ReactDOM 命名空间中,而不是像 createElement 那样的 React 命名空间。


一些细心的读者可能已经注意到,ReactDOM.createPortal 签名与 ReactDOM.render 是一样的,这样很容易记住。 但是,与 ReactDOM.render 不同,ReactDOM.createPortal 返回一个可渲染的子节点,该子节点在 协调 过程中使用。

何时使用

当父组件声明了 overflow: hidden 或具有影响 堆栈上下文 的属性并且您需要在视觉上“突破”其容器时,React 门户非常有用。 一些示例包括对话框、全局消息通知、悬停卡片和工具提示。

事件通过门户冒泡

React 文档很好地解释了这一点。

尽管门户可以位于 DOM 树中的任何位置,但它在其他方面表现得像一个普通的 React 子级。 无论孩子是否是门户,上下文等功能的工作方式完全相同,因为门户仍然存在于 React 树中,无论其在 DOM 树中的位置如何。

这包括事件冒泡。 从门户内部触发的事件将传播到包含 React 树中的祖先,即使这些元素不是 DOM 树中的祖先。

这使得侦听对话框、悬停卡片等中的事件就像它们在与父组件相同的 DOM 树中呈现一样简单。

例子

在下面的示例中,我们将利用 React 门户及其事件冒泡功能。

标记从以下内容开始。

<div class="PageHolder">
</div>
<div class="DialogHolder  is-empty">
  <div class="Backdrop"></div>
</div>
<div class="MessageHolder">
</div>

.PageHolder div 是我们应用程序的主要部分。 .DialogHolder div 将是呈现任何生成的对话框的位置。 .MessageHolder div 将是呈现任何生成的消息的位置。

因为我们希望所有对话框都显示在应用程序的主要部分上方,所以 .DialogHolder div 声明了 z-index: 1。 这将创建一个独立于 .PageHolder 的堆叠上下文的新堆叠上下文。

因为我们希望所有消息都显示在任何对话框上方,所以 .MessageHolder div 声明了 z-index: 1。 这将为 .DialogHolder 的堆叠上下文创建同级堆叠上下文。 尽管同级堆栈上下文的 z-index 具有相同的值,但由于 .MessageHolder 在 DOM 树中位于 .DialogHolder 之后,因此仍然会呈现我们想要的样子。

以下 CSS 总结了建立所需堆叠上下文的必要规则。

.PageHolder {
  /* Just use stacking context of parent element. */
  /* A z-index: 1 would still work here. */
}

.DialogHolder {
  position: fixed;
  top: 0; left: 0;
  right: 0; bottom: 0;
  z-index: 1;
}

.MessageHolder {
  position: fixed;
  top: 0; left: 0;
  width: 100%;
  z-index: 1;
}

该示例将包含一个 Page 组件,该组件将被渲染为 .PageHolder

class Page extends React.Component { /* ... */ }

ReactDOM.render(
  <Page/>,
  document.querySelector('.PageHolder')
)

因为我们的 Page 组件将分别将对话框和消息渲染到 .DialogHolder.MessageHolder 中,因此它需要引用这些持有者 div渲染时间。 我们有几种选择。

我们可以在渲染 Page 组件之前解析对这些持有者 div 的引用,并将它们作为属性传递给 Page 组件。

let dialogHolder = document.querySelector('.DialogHolder');
let messageHolder = document.querySelector('.MessageHolder');

ReactDOM.render(
  <Page dialogHolder={dialogHolder} messageHolder={messageHolder}/>,
  document.querySelector('.PageHolder')
);

我们可以将选择器作为属性传递给 Page 组件,然后解析 componentWillMount 中的引用以进行初始渲染,如果选择器更改,则在 componentWillReceiveProps 中重新解析。

class Page extends React.Component {

  constructor(props) {
    super(props);
    let { dialogHolder = '.DialogHolder',
          messageHolder = '.MessageHolder' } = props

    this.state = {
      dialogHolder,
      messageHolder,
    }
  }

  componentWillMount() {
    let state = this.state,
        dialogHolder = state.dialogHolder,
        messageHolder = state.messageHolder

    this._resolvePortalRoots(dialogHolder, messageHolder);
  }

  componentWillReceiveProps(nextProps) {
    let props = this.props,
        dialogHolder = nextProps.dialogHolder,
        messageHolder = nextProps.messageHolder

    if (props.dialogHolder !== dialogHolder ||
        props.messageHolder !== messageHolder
    ) {
      this._resolvePortalRoots(dialogHolder, messageHolder);
    }
  }

  _resolvePortalRoots(dialogHolder, messageHolder) {
    if (typeof dialogHolder === 'string') {
      dialogHolder = document.querySelector(dialogHolder)
    }
    if (typeof messageHolder === 'string') {
      messageHolder = document.querySelector(messageHolder)
    }
    this.setState({
      dialogHolder,
      messageHolder,
    })
  }

}

现在我们已经确保我们将拥有门户的 DOM 引用,我们可以使用对话框和消息来渲染页面组件。

就像 React 元素一样,React 门户是基于组件属性和状态呈现的。 对于此示例,我们将有两个按钮。 一种将创建对话门户,以便在单击时在对话框持有者中呈现,另一种将创建要在消息持有者中呈现的消息门户。 我们将在组件的状态中保留对这些门户的引用,这将在渲染方法中使用。

class Page extends React.Component {
  // ...

  constructor(props) {
    super(props);
    let { dialogHolder = '.DialogHolder',
          messageHolder = '.MessageHolder' } = props

    this.state = {
      dialogHolder,
      dialogs: [],
      messageHolder,
      messages: [],
    }
  }

  render() {
    let state = this.state,
        dialogs = state.dialogs,
        messages = state.messages

    return (
      <div className="Page">
        <button onClick={evt => this.addNewDialog()}>
          Add Dialog
        </button>
        <button onClick={evt => this.addNewMessage()}>
          Add Message
        </button>
        {dialogs}
        {messages}
      </div>
    )
  }

  addNewDialog() {
    let dialog = ReactDOM.createPortal((
        <div className="Dialog">
          ...
        </div>
      ),
      this.state.dialogHolder
    )
    this.setState({
      dialogs: this.state.dialogs.concat(dialog),
    })
  }

  addNewMessage() {
    let message = ReactDOM.createPortal((
        <div className="Message">
          ...
        </div>
      ),
      this.state.messageHolder
    )
    this.setState({
      messages: this.state.messages.concat(message),
    })
  }


  // ...
}

为了演示事件会从 React 门户组件冒泡到父组件,让我们在 .Page div 上添加一个点击处理程序。

class Page extends React.Component {
  // ...

  render() {
    let state = this.state,
        dialogs = state.dialogs,
        messages = state.messages

    return (
      <div className="Page" onClick={evt => this.onPageClick(evt)}>
        ...
      </div>
    )
  }

  onPageClick(evt) {
    console.log(`${evt.target.className} was clicked!`);
  }

  // ...
}

单击对话框或消息时,将调用 onPageClick 事件处理程序(只要另一个处理程序没有停止传播)。

请参阅上述演示的 工作示例

👉 当您遇到 overflow: hidden 或堆叠上下文问题时,请使用 React 门户!