在React中优雅地使用弹窗——withModal

这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战

如何快速地写弹窗

如果有这么一个需求:点击新建按钮,弹出表单并提交,你会怎么写?

image.png 很简单,六七十行代码就实现了。

import { Button, Form, Input, Modal, Select } from 'antd';
import './style.less';
import { useState } from 'react';

export default function TestPage() {
  const [visiable, setVisiable] = useState(false);
  const [form] = Form.useForm();
  // 打开弹窗
  const open = () => {
    setVisiable(true);
  };
  //关闭弹窗
  const close = () => {
    setVisiable(false);
  };
  //点击确定提交表单
  const submit = ()=>{
    form.submit()
  }
  // 提交后获取表单数据,请求接口,重置表单并关闭
  const onSubmit = (values) =>{
    console.log(values)
    //await  fetch ...
    form.resetFields();
    close()
  }
  return (
    <div>
      <div className="text-center">
        <Button type="primary" onClick={open}>
          新建
        </Button>
      </div>
      <Modal 
          wrapClassName="modal-wrap"
          okText="提交"
          cancelButtonProps={{ shape: 'round' }}
          okButtonProps={{ shape: 'round' }}
          width={600}
          visible={visiable}
          title="新建用户" 
          onCancel={close} 
          onOk={submit}
      >
        <div className="form">
          <Form form={form} labelCol={{ span: 4 }} wrapperCol={{ span: 16 }} onFinish={onSubmit}>
            <Form.Item
              label="用户名"
              name="username"
              rules={[{ required: true, message: 'Please input  username!' }]}
            >
              <Input />
            </Form.Item>
            <Form.Item
              label="用户邮箱"
              name="mail"
              rules={[{ required: true, message: 'Please input mail!' }]}
            >
              <Input />
            </Form.Item>
            <Form.Item
              label="部门"
              name="depart"
              rules={[{ required: true, message: 'Please input depart!' }]}
            >
              <Select>
                <Select.Option value={1}>市场部</Select.Option>
                <Select.Option value={2}>财务部</Select.Option>
                <Select.Option value={3}>研发部</Select.Option>
              </Select>
            </Form.Item>
          </Form>
        </div>
      </Modal>
    </div>
  );
}
复制代码

有问题吗?没有问题。

但是现在又来了一个需求,有10个地方都需要打开这个新建用户的弹窗。怎么办?copy呗!

这么做有问题吗?没有问题。

这时候又来了个需求:还需要一个新建部门的表单弹窗。怎么办?继续copy呗!

弹窗的标题改一下,表单的字段和提交改一下。

copy这么多有关系吗?没有关系。因为快啊。

可是现在又来了个需求:新建用户的表单要加一个字段

那么问题来了!我怎么知道我copy的代码分散在哪些文件?会不会改漏?

这时候就想起来了,我们还有一个杀手锏,叫复用

那么问题来了,怎么复用?

组件复用复用三部曲

组件复用,看起来简单,但很多人要么起来简单,用起来痛苦;要么做起来痛苦,用起来复杂。

就拿复用这个新建用户的弹窗来说吧,首先一个问题是你要复用什么

理清对象,确定复用内容

我们这里有三个对象:新建用户页面Page,打开弹窗的Button,弹窗Modal,表单From, 这三个对象分别有一些功能:

  • Page:提供弹窗打开关闭的功能(open/close),提供触发表单提交的方法(submit),提供form对象(Form.useForm())、接收表单输出并提交server
  • Button: 打开弹窗(调用open)
  • Modal: 提供点击取消和确定的事件(onCancel/onOk)
  • Form: 表单校验,提供onFinish事件输出表单值

这里最复杂、可变的就是弹窗和弹窗的内容,因此最需要复用的就是弹窗和它的内容(表单)。

那么阻碍我们的是什么呢?

Page需要form对象来提交Form,Form提交后需要关闭Modal。这些行为到底归属谁?

确定边界

这里最简单的对象就是Button了,它是个傻子,只有一个onClick动作,至于干啥,全看页面。

Modal也不复杂,它提供取消和确定的回调,至于内容也是外部提供的。我们复用一个Modal,就是用它的样式配置

Form:我们复用一个表单,自然也是希望能复用这个表单的提交逻辑。因此,除了表单校验外,它还应该承担请求server的职责。

拆离组件

封装useModal,拆离Modal:

useModal.tsx:

import { Modal } from 'antd';
import type { ModalProps } from 'antd';
import * as React from 'react';
import type { MutableRefObject } from 'react';
interface PropsType<T> extends Omit<ModalProps, 'onOk'> {
  onOk: (ref: MutableRefObject<T | undefined>) => void;
}
function withModal<T = any>(modalProps?: ModalProps, slotProps?: any) {
  return function (Slot: React.FC<any>) {
    return (props?: PropsType<T>) => {
      const ref = React.useRef<T>();
      return (
        <div>
          <Modal
            wrapClassName="modal-wrap"
            okText="提交"
            cancelButtonProps={{ shape: 'round' }}
            okButtonProps={{ shape: 'round' }}
            width={600}
            {...modalProps}
            {...props}
            onOk={() => props?.onOk?.(ref)}
          >
            <Slot {...slotProps} ref={ref} close={props?.onCancel} />
          </Modal>
        </div>
      );
    };
  };
}
export default withModal;

复制代码

拆离表单,暴露ref

UserForm.tsx:

import { Form, Input, Select } from 'antd';
import type { FormInstance } from 'antd/es/form';
import React from 'react';
type PropsType = React.PropsWithChildren<{
  afterSubmit?: (values: any, form: FormInstance<any>) => void;
}>;
const UserForm = (props: PropsType, ref?: React.ForwardedRef<FormInstance>) => {
  const [form] = Form.useForm();
  // 提交后获取表单数据,请求接口,重置表单并关闭
  const onSubmit = (values: any) => {
    console.log(values);
    //await  fetch ...
    form.resetFields();
    props.afterSubmit?.(values, form);
  };
  return (
    <div className="form">
      <Form
        onFinish={onSubmit}
        ref={ref}
        form={form}
        labelCol={{ span: 4 }}
        wrapperCol={{ span: 16 }}
      >
        <Form.Item
          label="用户名"
          name="username"
          rules={[{ required: true, message: 'Please input  username!' }]}
        >
          <Input />
        </Form.Item>
        <Form.Item
          label="用户邮箱"
          name="mail"
          rules={[{ required: true, message: 'Please input mail!' }]}
        >
          <Input />
        </Form.Item>
        <Form.Item
          label="部门"
          name="depart"
          rules={[{ required: true, message: 'Please input depart!' }]}
        >
          <Select>
            <Select.Option value={1}>市场部</Select.Option>
            <Select.Option value={2}>财务部</Select.Option>
            <Select.Option value={3}>研发部</Select.Option>
          </Select>
        </Form.Item>
      </Form>
    </div>
  );
};
export default UserForm;

复制代码

forwardRef

针对这里的特殊场景,form的提交由useModal控制,因此需要暴露form或ref;当然也可以由form自身提供submit按钮,并隐藏modal的footer,但这么做当表单较多时并不划算。

使用useModal:

 const UserFormModal = withModal({ 弹窗的props }, { 弹窗内容组件(表单)的props })(React.forwardRef(UserForm));
复制代码

最后再来看效果,这个页面少了一半的代码

export default function TestPage() {
  const [visiable, setVisiable] = useState(false);
  // 打开弹窗
  const open = () => {
    setVisiable(true);
  };
  //关闭弹窗
  const close = () => {
    setVisiable(false);
  };
  //点击确定提交表单
  const submit = (ref: MutableRefObject<FormInstance>) => {
    ref.current.submit();
  };
  const afterSubmit = () => {
    close();
  };
  const UserFormModal = withModal({ title: '新建用户' }, { afterSubmit })(React.forwardRef(UserForm));

  return (
    <div>
      <div className="text-center">
        <Button type="primary" onClick={open}>
          新建
        </Button>
      </div>
      <UserFormModal visible={visiable} onCancel={close} onOk={submit} />
    </div>
  );
}
复制代码

拥抱hooks

上述的做法是典型的HOC思想,如果我们用hooks,上面的代码还能再减掉50%,下一篇将介绍如何封装useModal

おすすめ

転載: juejin.im/post/7031508533820686350
おすすめ