创建一个React组件库时,类似于<Button/>
这类局部组件,是页面上的一个固定元素,所以相对来说,写起来比较直观,只需要定义一个组件,让用户直接去引用就可以了。但是对于"弹框"这种全局组件,应该如何写呢?应该如何控制组件的展示呢?
引用局部组件的思路是,让用户从组件层面控制弹框的展示,最后用户使用的代码大概是这个样子。如果displayModal
为false,则渲染null,从而使组件消失;如果为true,则渲染组件,让组件出现。
const App = ()=>{
const [displayModal,setDisplayModal] = useState(false);
return (
<div>
<button onClick={()=>setDisplayModal(!displayModal)}>Click</button>
{ displayModal ? <Modal/> : null }
</div>
)
}
但是这种带来的麻烦就是,组件位于在第一层<div>
内部,任何外层样式都可能会影响到组件的样式,导致每次都需要使用者去手动调整样式。
既然在这个层级内,样式会被干扰到,那么很自然地,需要把组件写到body下面。一种比较直接的方式当然就是在最上层组件里面,加一个Modal组件,然后下层去控制这个Modal的展示与否,大概是这样的:
const App = ()=>{
return (
<>
{/* 其他逻辑组件 */}
<Modal/>
</>
)
}
这样造成的一是如果有很多全局组件,则要在App下面写一堆组件,显得非常繁琐;二是逻辑分散,如果你写的是组件库,则相当于用户需要在它的最上层组件中去引用,然后在下层组件中去写控制逻辑;三是,如果这样写,Modal和下层组件被隔离开,需要另想办法来控制组件的展示与否
React的Portal可以达到,在组件中,可以突破组件层级,将某个组件渲染到一个层级外组件下面,作为那个组件的子组件。使用Portal的写法大概是:
const ConfirmModal = ({ visible = false }) => {
const Modal = (
<div className={cname('confirm-modal-container', { hidden: !visible })}>
{/* modal content */}
</div>
);
if (visible) {
return (
ReactDOM.createPortal(
Modal,
document.body
)
);
} else if (!visible) {
return null;
}
};
通过visible属性来控制组件的展示与否,如果可见,则返回一个ReactDOM.createPortal(Modal,document.body)
,否则返回null。在这种情况下,生成的Modal则会变为body的子元素,从而突破层级限制,Modal的样式也不会受到其他组件的影响,Modal的样式大概可以这样:
.confirm-modal-container{
position: fixed;
background: rgba(128, 128, 128, 0.5);
top: 0;
bottom: 0;
left: 0;
right: 0;
}
这样自然就形成了一个全局的蒙层。
另外,在上面情况下,初始是不可见,变为可见后,Modal会被生成称为body的直接子元素;当visible属性由true变为fasle时,由于最后渲染的是null,所以Modal会从body下被移除。
上面的情况已经基本可以达到要求了,但是,每次的可见性切换,都会执行DOM插入与移除操作。一般的可见性都可以通过类名控制,所以做一个小小的更新:
const ConfirmModal = ({ visible = false }) => {
const hasShow = useRef(false);
const Modal = (
<div className={cname('confirm-modal-container', { hidden: !visible })}>
{/* modal content */}
</div>
);
let portal = null;
if (visible || hasShow.current) {
if (!hasShow.current) {
hasShow.current = true;
}
portal = (
ReactDOM.createPortal(
Modal,
document.body
)
);
} else if (!visible && !hasShow.current) {
portal = null;
}
return portal;
};
如下:
useRef
设置一个hasShow
属性,初始值为false,用来表示弹框是否是第一次展示。如果visible为false,并且是第一次展示,则渲染null。hasShow
置为true,表示已经展示过。此时返回React.createPortal
,向body下插入ModalReact.createPortal
,只不过此时内部被增加一个hidden类,针对这个类设置display:none
,从而起到隐藏作用React.createPortal
,只不过去掉hidden
类。从而使插入Modal的body不用重复地被插入移除。