「✍ React Hook 封装一个Modal ?」

5,234 阅读1分钟

背景

最近在和同事合作一个需求,其中很多模块都需要一个相同风格的简单弹窗组件,而项目中引入的antd-mobile无法直接满足需求,最后决定由我来做一次二次封装。

开始

一开始我的理解是封装一层样式,新增一些特有的属性,其他的props透传就完事了,于是有了下面的组件 CommonModal

import React from 'react'
import Modal, { ModalProps } from 'antd-mobile'

interface IProps extends ModalProps {
  content: string
  commonModalClassName?: string
}

const CommonModal: React.FC<IProps> = (props) => {
  const { commonModalClassName = '', content = '', ...resProps} = props
  return (
     <div className={`common-modal ${ commonModalClassName }`}>
       <Modal {...resProps}>
         <p className="common-modal-content">{ content }</p>
       </Modal>
     </div>
  )
}

看起来简单粗暴没毛病。

使用Hook优化

功能已经实现,但同事用的时候反馈,作为一个简单的弹窗,每次用的时候还需要定义好state,还要引入CommonModal组件, 比较麻烦

const [visible, setVisible] = useState(false)
const [content, setContent] = useState(false
...

这时我想起来antd-mobileModal可以直接调用Modal.alert来唤起弹窗,对于多处复用的弹窗,像这样封装一个可执行的方法是不错的。先看看antd-mobile的实现。

export default function alert(){
  const div: any = document.createElement('div');
  document.body.appendChild(div);

  function close() {
    ReactDOM.unmountComponentAtNode(div);
    if (div && div.parentNode) {
      div.parentNode.removeChild(div);
    }
  }
  
  ReactDOM.render(
    <Modal
      visible
      transparent
      title={title}
      transitionName="am-zoom"
      closable={false}
      maskClosable={false}
      footer={footer}
      maskTransitionName="am-fade"
      platform={platform}
      wrapProps={{ onTouchStart: onWrapTouchStart }}
    >
      <div className={`${prefixCls}-alert-content`}>{message}</div>
    </Modal>,
    div,
  );
}

贴了部分核心代码,其实就是用ReactDOM来渲染弹窗,用dom.parentNode.removeChild来卸载弹窗。

用Hook实现

useCommonModal

import React, { useCallback, useRef } from 'react'
import ReactDOM from 'react-dom'
import CommonModal, { IProps as ModalProps } from './index'


const Modal: React.FC<ModalProps> = React.memo(({ visible, ...res }) => <CommonModal visible={ visible } { ...res } />)

type IProps = Pick<ModalProps, 'mainText' | 'subText' | 'btnText' | 'type' | 'onClose'>

const useCommonModal = (
  props: IProps,
): {
  closeModal: () => void
  showModal: () => void
} => {

  const domRef = useRef(null)

  /**
   *  关闭弹窗
   */
  const closeModal = useCallback(() => {

    const dom = domRef.current

    ReactDOM.unmountComponentAtNode(dom)

    if (dom && dom.parentNode) {
      dom.parentNode.removeChild(dom)
    }

    const { onClose } = props
    if (typeof onClose === 'function') {
      onClose()
    }
  }, [props])

  /**
   *  展示弹窗
   */
  const showModal = useCallback(() => {
    const Root = document.body

    if (!domRef.current) {
      // 创建一个dom
      domRef.current = document.createElement('div')
      Root.appendChild(domRef.current)
    }

    const dom = domRef.current

    const ele = React.createElement(
      Modal,
      {
        visible: true,
        onClose: closeModal,
        onClickBtn: closeModal,
        maskClosable: true,
        ...props,
      },
      null,
    )
    ReactDOM.render(ele, dom)
  }, [closeModal, props])

  return {
    closeModal,
    showModal,
  }
}

export default useCommonModal

使用

const { show, close } = useCommonModal({
  title: '',
  content: '',
  btnText: '',
})