译:用实例理解React Portals
原文:React Portals — Understanding with examples
引言:在React中正确放置DOM元素指北
图片:瑞克和莫蒂从传送门走出,图源Pinterest
在架构可扩展的应用程序时,仅仅编写语法正确的React代码是远远不够的。写出语义正确的代码同样也很重要,这恰恰是React Portals的闪光点。即使你已经掌握了DRY(Don't Repeat Yourself)编码原则,也必须确保你的代码遵循最佳编码实践。
我所说的“正确放置”是指最终在浏览器中应该呈现合理的HTML DOM。我将先用对话框的例子来解释这一点,ToolTip和悬浮卡片同样适用此例子。
如果你是一名开发者,你一定会承认这样一个事实:在处理模态和对话框过程中体验可能会极差,浮层和背景会极大地考验你的耐心。现在假设你有一个可重用的模态功能组件然后你想在其他各种组件中复用它,组件如下:
import React, { Fragment } from 'react';
import classes from "./Modal.module.css";
const Backdrop = (props) => {
return <div className={classes.backdrop} onClick={props.onClose}></div>;
};
const ModalOverlay = (props) => {
return (
<div className={classes.modal}>
<div className={classes.content}>{props.children}</div>
</div>
);
};
const Modal = (props) => {
return (
<Fragment>
<Backdrop onClose={props.onClose} />
<ModalOverlay>{props.children}</ModalOverlay>
</Fragment>
);
};
export default Modal;
.backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
z-index: 20;
background-color: rgba(0, 0, 0, 0.75);
}
.modal {
position: fixed;
top: 20vh;
left: 5%;
width: 90%;
background-color: white;
padding: 1rem;
border-radius: 14px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
z-index: 30;
animation: slide-down 300ms ease-out forwards;
}
然后把它引入App.js
import React from "react";
import { render } from "react-dom";
import Modal from "./Modal/Modal";
// 函数组件
function Parent(props) {
return (
<div>
<h3>This is parent</h3>
<Modal>
<h5>I am child in modal</h5>
</Modal>
</div>
);
}
// 主体App组件
// 渲染消息列表,数据来源于messages.json
const App = (props) => {
return (
<div>
<h2>React Portals by Aditya Tyagi</h2>
<Parent />
</div>
);
};
render(<App />, document.getElementById("root"));
结果会是这样:
图:没有使用React Portals的模态窗口
现在,如果你仔细检查这里的DOM结构的话,你会看到它渲染在Parent组件里面。
图:模态被渲染在Parent里的DOM
像这样模态在Parent组件里面打开的形式,本身并没有错,但是从DOM的角度来看的话,是有语义问题的。在这个案例中,我们只有一个Parent,但是假设这个模态被放在了一个深度嵌套的Child组件中的话,它的DOM就会是这样的:
图:模态在一个深度嵌套的child组件中打开
解决方案:React Portals
理想情况下,Modal应该并列放置在root div旁边,body开始的地方,同时不能影响我们在深度嵌套的Child组件中方便使用Modal组件。
图:理想情况下,Modal渲染在DOM中的位置
这正是React Portal能解决的问题。
根据官方文档:
React Portals提供了一种一流的方式,可以将子节点渲染成存在于父组件的DOM层次之外的DOM节点。
API十分简单明了,方法createPortal
可以从ReactDom
中引入使用。
ReactDOM.createPortal(child, container)
这个方法接收两个入参:
child
你想渲染的任何东西,比如一个元素、字符串或片段。在我们的例子中,我们想渲染两个JSX元素,Backdrop和ModalOverlay,这两个元素是我们要传递的第一个参数
container
这是你要渲染你在第一个参数中传递的子元素的地方。这里可以用原生JavaScript方法document.getElementById()
来定位你想渲染元素的位置。
const modalPlaceholderElement = document.getElementById("modal-placeholder");
接着,既然我们想把Modal和背景并列渲染在根元素边上,我们得加一个带id的占位div,以方便我们后续定位它,就像这样
<div id="modal-placeholder"></div>
API使用大致如下:
ReactDom.createPortal(\
<Backdrop onClose={props.onClose} />, // 渲染元素
modalPlaceholderElement // 渲染位置
)
对组件进行必要的修改以及整合以上所有内容后:
I)Index.html
<body>
<!--Modal占位-->
<div id="modal-placeholder"></div>
<div id="root"></div>
</body>
II) Modal.js
import React, { Fragment } from "react";
import classes from "./Modal.module.css";
import ReactDom from "react-dom";
// 无改动
const Backdrop = (props) => {
return <div className={classes.backdrop} onClick={props.onClose}></div>;
};
// 无改动
const ModalOverlay = (props) => {
return (
<div className={classes.modal}>
<div className={classes.content}>{props.children}</div>
</div>
);
};
// 从index.html获得Modal占位符div
const modalPlaceholderElement = document.getElementById("modal-placeholder");
const Modal = (props) => {
return (
<Fragment>
{/* 用createPortal在占位符渲染子组件 */}
{ReactDom.createPortal(
<Backdrop onClose={props.onClose} />,
modalPlaceholderElement
)}
{/* 用createPortal在占位符渲染子组件 */}
{ReactDom.createPortal(
<ModalOverlay>{props.children}</ModalOverlay>,
modalPlaceholderElement
)}
</Fragment>
);
};
export default Modal;
这样也能产出相同的结果,但是DOM结构是不一样的:
图:通过React Portals,Modal渲染在了根div旁边
同样的原则也适用于Tooltips、悬浮卡片和对话框。React Portals将有助于创建结构合理和可扩展的APP,另外一个帮助你React代码在语义上正确的的方式是使用React的Fragment组件。
译者总结:
作者从DOM语意层面,指出了Modal对话框被实际渲染在了子组件里面的问题,并引出了React Portals的作用。通过React Portals,可以做到在开发中,类似的组件仍旧可以以组件嵌套的方式在其他组件中复用,但是在浏览器渲染时,是直接处于body标签以下、root标签并列的位置(Portal本身含义即传送门,在此一个生动形象的比喻可以是把层层嵌套的Modal组件在实际渲染时,【传送】到body标签下面),从语义角度来说更具合理性。
其实React Portals能解决的不仅是语义问题,译者通过调研其他国内外文章,例如 传送门:React Portal中提到,对话框一般需要显示在屏幕中央的位置。当这个对话框被包在其他组件中时,需要用CSS的position属性控制这个对话框的位置。我们知道position这个属性是absolute时,是会去寻找相对于它最近的一个已定位的元素。一般来说对话框是相对于当前viewport居中,这就要求从对话框往上一直到body没有其他position为relative的元素干扰。虽然但是,谁能保证所有其他的组件不用position呢?另外,因为包在了其他元素中,各种样式纠缠,CSS容易搞成一坨浆糊。
还有 Building a modal in React with React Portals中提到了另一种情况,当父组件拥有overflow:hidden属性或包含有更高层级的元素时,那么这个子组件就不能显示在最顶层,被限制在了父组件的可视区域。虽然我们可以通过设置很高的z-index值来把它抬升到最顶层,但是这样的实践往往不够优雅且不一定每次都能成功。
对于这两种情况,React Portals就能很好地解决了。
转载自:https://juejin.cn/post/7077768972950568967