Portal 提供了一种将子节点渲染到存在于父节点以外的 DOM 节点的优秀的方案。
ReactDOM.createPortal(child,container)
第一个参数 child 是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment 第二个参数是一个 DOM 元素 。
用法
通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点:
render(){
// React 挂载了一个新的div ,并且把子元素渲染其中
return(
<div>
{this.props.children}
</div>
);
}
然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的:
render() {
// React 并没有 创建一个新的 div 。他只是把子元素渲染到 ‘domNode’ 中。
// 'domNode' 是一个可以在任何位置的有效 DOM 节点。
return ReactDOM.createPortal(
this.props.children,
domNode
);
}
一个 portal 的典型用例是当父组件有 overflow:hidden 或 z-index 样式时,但你需要子组件能够在视觉上“跳出” 其容器。例如,对话框,悬浮卡以及提示框:
注意
当在使用 portal 时,记住 管理键盘焦点就很重要。
对于模态对话框,通过遵循 WAI-ARIA 模态开发实践,来确保每个人都能够运用它。
通过 Portal 进行实践冒泡
尽管 portal 可以被放置在DOM 树中的任何地方,但在任何其他方面,其行为和普通的 React 子节点行为一致。由于 portal 任存在于 React 树,且与 DOM 树中的位置无关,那么无论其子节点是否是 portal ,像 context 这样的功能特性都是不变的。
这包含事件冒泡。一个从 portal 内部触发的事件会一直冒泡至包含 React 树的祖先,即使这些元素并不是 DOM 树中的祖先。假设存在如下 HTML 结构:
<html>
<body>
<div id="app-root"></div>
<div id="model-root"></div>
</body>
</html>
在 #app-root 里的 Parent 组件能够捕获到未被捕获的兄弟节点 #modal-root 冒泡上来的事件。
//在 DOM 中有两个容器是兄弟级 (siblings)
const appRoot = document.getElementById('app-root');
const modalRoot = document.getElementById('modal-root');
class Modal extends React.Component {
constructor(props){
super(props);
this.el = document.createElement('div');
}
componentDidMount(){
// 在 Modal 的所有子元素被挂载后,
// 这个 portal 元素会被嵌入到 DOM 树中,
// 这意味着子元素将被挂载到一个分离的 DOM 节点中。
// 如果要求子组件在挂载时可以立刻接入 DOM 树,
// 例如衡量一个DOM 节点,
// 或者在后代节点中使用 ‘autoFocus’,
// 则需添加 state 到 Modal 中,
// 仅当 Modal 被插入 DOM 树中才能渲染子元素。
modalRoot.appendChild(this.e1);
}
componentWillUnmount(){
modalRoot.removeChild(this.e1);
}
render(){
return ReactDOM.createPortal(
this.props.children,
this.e1,
);
}
}
class Parent extends React.Component{
constructor(props) {
super(props);
this.state = {click:0};
this.handleClick = this.handClick.bind(this);
}
handleClick() {
// 当子元素里的按钮被点击时,
// 这个将会被触发更新父元素的 state,
// 即使这个按钮在 DOM 中不是直接关联的后代
this.setState(state => ({
click: state.clicks + 1
}));
}
render() {
return (
<div onClick={this.handleClick}>
<p> Number of clicks:{this.state.clicks}</p>
<p>
Open up the browser DevTools
to observe that the button
is not a child of the div
with the onClick handler.
</p>
<Modal>
<Child />
</Modal>
</div>
);
}
}
function Child() {
// 这个按钮的点击事件会冒泡到父元素
// 因为这里没有定义 'onclick' 属性
return (
<div className="modal">
<button>Click</button>
</div>
);
}
ReactDOM.render(<Parent />,appRoot);
在父组件里捕获一个来自 portal 冒泡上来的事件,使之能够在开发时具有不完全依赖于 portal 的更为灵活的抽象。例如,如果你在渲染一个 <Modal />
组件,无论其是否采用 portal 实现,父组件都能够捕获其事件。