React 命令式组件:Modal在 React 开发中,我们的固有思路是数据驱动视图的,但是对于 Toast、Moda
开始
在 React 开发中,我们的固有思路是数据驱动视图的,但是对于 Toast、Modal 等使用方式更加灵活的组件,或许可以搭配 DOM 操作来添加 Modal.open、Toast.info 等功能。
实现
为了实现类似 antd 中既支持 jsx 语法,又支持 Modal.open 的 Modal 组件,除了常规的 React Component 代码,我还使用了 reactDom 中的 createRoot 方法,在调用 Modal.open 时动态添加一个 React 根节点来渲染 Modal 组件。
以下是代码实现,样式部分使用了 tailwindcss
import { useEffect, useRef, useState } from 'react';
import { createRoot, Root } from 'react-dom/client';
interface ModalContentProps {
title?: string;
closable?: boolean;
children?: React.ReactNode;
onClose?: () => void;
}
interface ModalProps extends ModalContentProps {
visible?: boolean;
}
export default function Modal(props: ModalProps) {
const [visible, setVisible] = useState(props.visible);
const withCmd = isOpenWithCmd();
const close = () => {
if (withCmd) Modal.cancel();
else setVisible(false);
props.onClose?.();
};
const onMaskClick = (e: React.MouseEvent) => {
if (props.closable !== false) {
const target = e.target as HTMLElement;
if (target.getAttribute('data-type')?.includes('modal-wrapper')) close();
}
};
useEffect(() => {
if (!withCmd) {
if (visible && !props.visible) setVisible(false);
else if (!visible && props.visible) setVisible(true);
}
}, [props.visible]);
return (
<div
className={`fixed left-0 top-0 z-10 flex h-screen w-screen items-center justify-center bg-black bg-opacity-60 ${!visible && !withCmd ? 'hidden' : ''}`}
data-type='modal-wrapper'
onClick={onMaskClick}
>
<div className='min-w-80 rounded-md bg-white'>
{props.title ? <div className='border-b p-4 text-xl font-bold'>{props.title}</div> : null}
{props.children ? <div className='p-2 text-lg'>{props.children}</div> : null}
</div>
</div>
);
}
let container: HTMLDivElement | null = null;
let root: Root | null = null;
Modal.open = (props: ModalContentProps) => {
container = document.createElement('div');
container.setAttribute('data-type', 'modal-root');
root = createRoot(container);
root.render(<Modal {...props} />);
document.body.appendChild(container);
};
Modal.cancel = () => {
if (container && root) {
root.unmount();
document.body.removeChild(container);
container = root = null;
}
};
/**
* 通过 command 打开,会比通过 state 多一层容器,可以借此判断打开方式
*/
const isOpenWithCmd = () => !!document.body.querySelector('[data-type="modal-root"]');
总结
我通过 createRoot 的能力,为 Modal 组件添加了命令式控制的能力,让其可以摆脱 jsx 和 state 的限制,在使用方式上更加灵活,提高了简单场景下的开发效率。
同理,Toast、Message、Notification 等组件也可以通过这种方式实现。
转载自:https://juejin.cn/post/7416657615369502772