likes
comments
collection
share

React 命令式组件:Modal在 React 开发中,我们的固有思路是数据驱动视图的,但是对于 Toast、Moda

作者站长头像
站长
· 阅读数 52

开始

在 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
评论
请登录