React 通用弹窗 CommonModal

708 阅读2分钟

背景

弹窗是我们用的比较多的组件,假设我们要实现一个弹窗表单,单击某个按钮触发这个弹窗,这种在增删改查的场景下特别多。

传统做法

首先,我们会将表单作为一个组件来封装,里面使用Modal组件

// user/UserForm.jsx
const UserForm = ({onConfirm, onCancel}) => {
  return <Modal>....
    <Button onClick={onConfirm}>确定</Button>
    <Button onClick={onCancel}>取消</Button>
  </Modal>
}

其次,在主页面使用这个组件,声明一个变量showUserForm控制弹窗显示与否

import UserForm from './UserForm';

const User = () => {
  const [showUserForm, setShowUserForm] = useState(true);
  
  // 点击弹窗
  const handleClick = () => {
    setShowUserForm(true);
  }
  // 关闭弹窗
  const handleClose = () => {
    setShowUserForm(false);
  }
  return <>
    //... 其它逻辑
    <Button onClick={handleClick}>新增用户</Button>
    showUserForm && <UerForm  />
  <>
}

最后,给弹窗组件增加两个回调函数onConfirm、onCancel

import UserForm from './UserForm';

const User = () => {
  const [showUserForm, setShowUserForm] = useState(true);
  
  // 点击弹窗
  const handleClick = () => {
    setShowUserForm(true);
  }
  // 关闭弹窗
  const handleClose = () => {
    setShowUserForm(false);
  }
  // 用户点击【确定】回调
  const onConfirm = () => {
  }
  // 用户点击【取消】回调
  const onCancel = () => {
    handleClose();
  }
  return <>
    //... 其它逻辑
    <Button onClick={handleClick}>新增用户</Button>
    showUserForm && <UerForm onConfirm={onConfirm} onCancel={onCancel} />
  <>
}

问题

以上的做法,有两个问题:1. 通过一个变量去控制弹窗显示与否不够优雅;2. 逻辑太分散,一个弹窗需要使用四个方法、一个变量;3. 如果这个页面有多个弹窗的需求,那么逻辑会更复杂,导致不可维护

那么我们有没有更优雅的方式处理这类问题的,答案肯定是有的,不然也不会有这篇文章了

期望做法

首先,用户表单组件没有变,完全不用改

// user/UserForm.jsx
const UserForm = ({onConfirm, onCancel}) => {
  return <Modal>....
    <Button onClick={onConfirm}>确定</Button>
    <Button onClick={onCancel}>取消</Button>
  </Modal>
}

其次,引入CommonModal,不需要通过变量来控制弹窗显示与否

import UserForm from './UserForm';
import CommonModal from './CommonModal';

const User = () => {
  const store = useMoalStore();
  
  // 点击弹窗
  const handleClick = () => {
   store.openModal(<UserForm/>);
  }

  return <>
    //... 其它逻辑
    <Button onClick={handleClick}>新增用户</Button>
    <CommonModal store={store} />
  <>
}

最后,补充其它逻辑

import UserForm from './UserForm';
import CommonModal from './CommonModal';

const User = () => {
  const store = useMoalStore();
  
  // 点击弹窗
  const handleClick = () => {
   const onConfirm = () => {
   }
   store.openModal(<UserForm onConfirm={onConfirm}, onCancel={()=>{store.closeModal()}}/>);
  }

  return <>
    //... 其它逻辑
    <Button onClick={handleClick}>新增用户</Button>
    <CommonModal store={store} />
  <>
}

优势

这种写法带来几个变化:1. 不需要通过变量去控制弹窗,而是通过store对象去控制;2. 逻辑集中,所有逻辑都可以集中在一个handleClick,包括弹窗组件、两个回调函数、回调逻辑,阅读代码更方便;3. 支持多个弹窗,如果这个页面还有其它弹窗,那么不需要再写一个 ,它完全是可以复用的,任何地方都可以使用store.openMoal打开一个弹窗

CommonModal实现

它是一个公共组件,放在公共组件目录下

首先是CommonModal/index.jsx组件,它很简单,就是监听visible的变化,然后如果为true就render表单组件,如果为false就隐藏表单组件

// CommonModal/index.jsx 

import React, { useEffect, useState } from 'react';

import useModalStore from './useModalStore';
import Store from './store';

export { useModalStore as useModalStore };
export { Store as Store };

const CommonModal = props => {
  const { store } = props;
  const [visible, setVisible] = useState(false);

  useEffect(() => {
    // eslint-disable-next-line no-underscore-dangle
    store.__onVisibleChanged = value => {
      setVisible(value);
    };
  }, [store]);

  if (visible) {
    return store.Component;
  } else {
    return null;
  }
};

export default CommonModal;

其次是Store类,仅提供两个方法,其中openModal会将表单组件存在实例变量,并触发CommonModal更新

/** @format */

class Store {
  __onVisibleChanged = null;

  openModal(Component, config = {}) {
    this.Component = Component;
    this.__onVisibleChanged && this.__onVisibleChanged(true);
  }

  closeModal() {
    this.__onVisibleChanged && this.__onVisibleChanged(false);
  }
}

export default Store;

最后是一个useModalStore hooks,很简单,就暴露一个单例的store

import { useMemo } from 'react';
import Store from './store';

const useModalStore = () => {
  const store = useMemo(() => new Store(), []);

  return store;
};

export default useModalStore;

总结

使用这种方式,能让代码更紧凑、可读性更好,我见过一个文件有好多个弹窗,每个弹窗的逻辑还很复杂,不了解这块逻辑的代码是很难读懂的。 用了这种方法之后,会让代码更简洁,也更容易拆分和复用逻辑