此文章是翻译Portals这篇React(版本v16.2.0)官方文档。

Portals

Portal 提供最好的方式去渲染子节点到父组件DOM 层级之外的DOM 节点上。

  1. ReactDOM.createPortal(child, container)

第一个参数(child)是任何能够渲染的React 子节点,像元素、字符串、或者fragment。第二个参数(container)是一个DOM 元素。

Usage

通常讲,当你从组件的render 方法返回一个元素,该元素仅能加载到离其最近的父节点的子元素的DOM:

  1. render() {
  2. // React mounts a new and renders the children into it
  3. return (
  4. <div>
  5. {this.props.children}
  6. </div>
  7. )
  8. }

然而,有时候将其插入到子节点到DOM 的不同位置也是有用的:

  1. render() {
  2. // React does *not* create a new div. It renders the children into `domNode`.
  3. // `domNode` is any valid DOM node, regardless of its location in the DOM.
  4. return ReactDOM.createPortal(
  5. this.props.children,
  6. domNode,
  7. );
  8. }

对于portal 的一个典型用例是当父组件有overflow: hiddenz-index 样式,但你需要子组件能够在视觉上“跳出(break out)”其容器。例如,对话框、hovercards以及提示框。

注意:

记住这点非常重要,当在使用portals时,你需要确保遵循合适的可访问指南。

在CodePen 上尝试

Event Bubbling Through Portals

即使portal 可以在DOM 树中的任何位置,它的行为都像普通的React 子节点。无论子节点是否为portal,类似于context 的功能都是完全相同的,因为无论DOM 树中的位置如何,portal 仍然存在于React 树中。

这包括事件冒泡。从portal 内部触发的事件将传播到包含React 树中的祖先,即使这些元素不是DOM 树中的祖先。假设以下HTML 结构:

  1. <html>
  2. <body>
  3. <div id="app-root"></div>
  4. <div id="modal-root"></div>
  5. </body>
  6. </html>

#app-root 里的Parent 组件能够捕获到一个未被捕获的,从兄弟节点#modal-root 冒泡上来的事件。

  1. // These two containers are siblings in the DOM
  2. const appRoot = document.getElementById('app-root');
  3. const modalRoot = document.getElementById('modal-root');
  4. class Modal extends React.Component {
  5. constructor(props) {
  6. super(props);
  7. this.el = document.getElementById('div');
  8. }
  9. componentDidMount() {
  10. // The portal element is inserted in the DOM tree after
  11. // the Modal's children are mounted, meaning that children
  12. // will be mouted on a detached DOM node. If a child
  13. // compoennt requires to be attached to the DOM tree
  14. // immediately when mounted, for example to measure a
  15. // DOM node, or uses 'autoFocus' in a descendant, add
  16. // state to Modal and only render the children when Modal
  17. // is inserted in the DOM tree.
  18. modalRoot.appendChild(this.el);
  19. }
  20. componentWillUnmount() {
  21. modalRoot.removeChild(this.el);
  22. }
  23. render() {
  24. return ReactDOM.createPoral(
  25. this.props.children,
  26. this.el,
  27. );
  28. }
  29. }
  30. class Parent extends React.Component {
  31. constructor(props) {
  32. super(props);
  33. this.state = {clicks: 0};
  34. this.handleClick = this.handleClick.bind(this);
  35. }
  36. handlClick() {
  37. // This will fire when the button in child is clicked,
  38. // updating Parent's state, even though button
  39. // is not direct descendant in the DOM.
  40. this.setState(prevState => ({
  41. clicks: prevState.clicks + 1
  42. }));
  43. }
  44. render() {
  45. return (
  46. <div onClick={this.handleClick}>
  47. <p>Number of clicks: {this.state.clicks}</p>
  48. <p>
  49. Open up the browser DevTools
  50. to observe that the Button
  51. is not a child of the div
  52. with the onClick handler.
  53. </p>
  54. <Modal>
  55. <Child />
  56. </Modal>
  57. </div>
  58. );
  59. }
  60. }
  61. function Child() {
  62. // The click event on this button will bubble up to parent,
  63. // because there is no 'onClick' attribute defined
  64. return (
  65. <div className="modal">
  66. <button>Click</button>
  67. </div>
  68. );
  69. }
  70. ReactDOM.render(<Parent />, appRoot);

在CodePen 上尝试

在父组件里捕获一个来自portal 的事件冒泡,允许不固有的依赖于portal 的更为灵活的抽象的开发。例如,若你在渲染一个<Modal /> 组件,父组件能够捕获其事件而无论其是否采用portal 实现。