由浅入深,封装一个带有输入框的模态框Hooks

背景

在大型项目的开发中,很多地方都需要弹出理由/原因的输入框弹窗,比如用户取消订单时输入取消原因,或者后台拉黑时写明拉黑理由。每次都要写一堆相似的代码来控制弹窗的显示、隐藏和处理用户输入,一旦面临改动,就得到处找代码去修改,非常之麻烦。

出于组件化考虑,大家肯定会在项目中封装一个类似于 reasonModal 的组件,实现一个带有输入框的模态框组件封装,用于各个场景的复用,这也是比较直觉的JSX组件的封装设计。在本文中,除了基础的JSX组件版本的模态框设计,更是优化了一版 Hooks 方式的组件封装 useReasonModal,将模态框的显示逻辑、状态管理和回调处理封装起来,使得这些逻辑可以在不同的组件中复用,同时保持组件的简洁。

JSX组件版设计

先来看看使用JSX组件方式封装设计的组件:

首先,定义需要封装的 ReasonModal 组件:

jsx 复制代码
import React, { useState, useEffect } from 'react';
import { Modal, Input, message } from 'antd';

// 入参需要包括模态框的全部展示信息及操作回调函数
const ReasonModal = ({ title, visible, onOk, onCancel}) => {
  const [reason, setReason] = useState('');

  useEffect(() => {
    if (!visible) {
      setReason(''); // 重置原因输入框
    }
  }, [visible]);

  const handleOk = async () => {
    if (!reason) {
      message.error('请输入原因');
      return;
    }
    try {
      await onOk(reason, record);
      onCancel(); // 关闭模态框
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <Modal
      title={title}
      visible={visible}
      onCancel={onCancel}
      onOk={handleOk}
    >
      <Input.TextArea
        placeholder="请输入原因"
        value={reason}
        onChange={(e) => setReason(e.target.value)}
      />
    </Modal>
  );
};

export default ReasonModal;

然后,在父组件中使用 ReasonModal 组件:

jsx 复制代码
import React, { useState } from 'react';
import ReasonModal from './ReasonModal';

const ParentComponent = () => {
  const [isModalVisible, setIsModalVisible] = useState(false);

  const handleOpenModal = () => {
    setIsModalVisible(true);
  };

  const handleCancel = () => {
    setIsModalVisible(false);
  };

  const handleOk = async (reason, record) => {
    console.log('提交原因/拉黑理由:', reason);
    // 这里可以添加提交原因后的逻辑,比如发送到服务器等
    setIsModalVisible(false);
  };

  return (
    <div>
      <button onClick={() => handleOpenModal()}>
        取消/拉黑(打开弹窗)
      </button>
      <ReasonModal
        title="请输入取消/拉黑原因"
        visible={isModalVisible}
        onOk={handleOk}
        onCancel={handleCancel}
      />
    </div>
  );
};

export default ParentComponent;

在以上示例中,ReasonModal 是一个独立的组件,它可以接收 titlevisibleonOkonCancel 作为属性。父组件 ParentComponent 控制模态框的打开和关闭,并处理提交输入内容的逻辑。

JSX组件 的封装中,ReasonModal 组件是一个独立的 React 组件,它直接接收父组件传递的 props 来管理状态和逻辑。它的缺点也在于此,每次使用时都需要在父组件中重复状态管理和逻辑处理的代码,必然会增加代码量和维护成本。

同时,假设我们的应用场景是在一个列表中的某栏中的操作属性,当我们在上传输入框中的内容时,同时需要参考到本行数据的值 record。为了将 record 中的值传给模态框组件,那么在父组件中就又需要多管理一个状态值,并在函数 handleOpenModal() 中去改变状态值,并在 ReasonModalprops 中添加传递的 record 数据。

如何进一步提高这个组件的复用性和封装性呢? 如何将模态框的逻辑和状态管理隐藏在组件内部,使得调用它的父组件更加简洁呢? 如何在需要使用到 record 甚至其他配置值时,不依赖于父组件的状态管理呢? 接下来,我们来用 React 的自定义 Hooks 来一起实现一个满足上述功能的 useReasonModal

Hooks版设计

直接上代码:

ts 复制代码
type ReasonModal = <T>(props: {
  title: string;
  onOk: (reason: string, record: T) => Promise<void>;
}) => [(record?: T) => void, JSX.Element];

export const useReasonModal: ReasonModal = ({ title, onOk }) => {
  type T = Parameters<typeof onOk>[1];
  const [visible, { setFalse, setTrue }] = useBoolean();
  const [reason, setReason] = useState('');
  const [record, setRecord] = useState(undefined as unknown as T);
  // 弹窗关闭之后需要重置数据
  useEffect(() => {
    if (!visible) {
      setReason('');
      setRecord(undefined as unknown as T);
    }
  }, [visible]);
  return [
    (record) => {
      if (record) setRecord(record);
      setTrue();
    },
    <Modal
      visible={visible}
      title={title}
      onCancel={setFalse}
      onOk={() => {
        if (!reason) {
          message.error('请输入原因');
          return;
        }
        // 传入的onOk函数,可选链式调用,参数为输入框内容reason及行数据record
        onOk?.(reason, record).then(setFalse, log);
      }}
    >
      <Input.TextArea
        placeholder="请输入原因"
        value={reason}
        onChange={(e) => {
          setReason(e.target.value);
        }}
      />
    </Modal>,
  ];
};

使用案例如下:

ts 复制代码
interface User {
  id: number;
  name: string;
  email: string;
}

const [confirm, confirmReasonModal] = useReasonModal<User>({
  title: "确认拉黑吗",
  onOk: (reason, user) => {
    // 发送拉黑的请求
    return request(user.id, reason);
  },
});


...

const columns = {
    ...,
    {
      title: '操作',
      render(_, record) {
        return (
          <>    
            <Button
              type="link"
              onClick={() => {
                confirm(record);
              }}
            >
              确认按钮
            </Button>
          </>
        )
      },
    },
}

return (
    <>
      <Table columns={columns} />
      {confirmReasonModal}
    </>
)

ReasonModal 类型设计

在这段代码中:

typescript 复制代码
type ReasonModal = <T>(props: {
  title: string;
  onOk: (reason: string, record: T) => Promise<void>;
}) => [(record?: T) => void, JSX.Element];

ReasonModal 被定义为一个泛型函数类型,其中 T 是一个泛型类型参数。这个函数接收一个对象 props 作为参数,该对象包含两个属性:

  • title: 字符串,用于设置模态框的标题。
  • onOk:函数,这个函数接受两个参数:一个 string 类型的 reason ,代表输入框的内容;和一个 T 类型的 record,代表需要使用到的行数据对象;并返回一个 Promise<void>,这允许在模态框内进行异步操作,如API调用。返回值类型[(record?: T) => void, JSX.Element]表示useReasonModal返回一个元组,第一个元素是一个函数,用于显示模态框,可选地接受一个类型为T的记录对象;第二个元素是一个JSX.Element,即模态框组件。

这里的 T 代表 record 参数的类型,它是一个泛型,这意味着 T 可以是任何类型。这样设计的目的是为了让 ReasonModal 类型更加灵活和可重用,因为它可以用于不同类型的 record 参数。

record 类型设置为 T 允许调用者在使用 ReasonModal 时指定 record 的具体类型。例如,如果你有一个特定的数据类型来表示用户信息,你可以将 T 指定为那个用户信息类型,这样 onOk 函数就可以接收一个具有正确类型的 record 参数,并且 ReasonModal 返回的函数也将接受相同类型的 record 参数。

这种泛型设计使得 ReasonModal 可以在不同的上下文中使用,而不必为每种可能的 record 类型编写特定的代码,进而提高了代码的复用性和类型安全性。

其他注意

  1. onOk 最好返回一个Promise,结果reject时弹窗不会关闭 结果resolve时会自动关闭
  2. 优雅地管理模态框显隐的布尔值状态 ahooks文档

需要扩展补充信息时的设计

如果我们希望在现有的 useReasonModal 基础上,继续多传入一些参数,使得模态框 Modal 的 title 和 placeholder 可以根据 onOK 回调函数传入的参数进行配置,该如何扩展改造useReasonModal呢?

想要让 Modal 的 title 或 placeholder 根据 record 的值进行动态变化,又不想在 useReasonModal 组件内部对代码进行非公有性的数据处理,那就需要在 useReasonModal 返回的函数的参数中增加配置参数,接受经过判断或改造后的 title 或 placeholder。

直接上代码:

typescript 复制代码
import React, { useState, useEffect } from 'react';
import { Modal, Input, message } from 'antd';
import { useBoolean } from 'ahooks';

type ReasonModal = <T>(props: {
  title: string;
  placeholder: string;
  onOk: (reason: string, record: T) => Promise<void>;
}) => [(record?: T, config?: any) => void, JSX.Element];

export const useReasonModal = ({ title, placeholder, onOk }) => {
  type T = Parameters<typeof onOk>[1];
  const [visible, { setFalse, setTrue }] = useBoolean();
  const [reason, setReason] = useState('');
  const [record, setRecord] = useState(undefined as unknown as T);
  const [modalConfig, setModalConfig] = useState({
    title: title,
    placeholder: placeholder
  });

  // 弹窗关闭之后需要重置数据
  useEffect(() => {
    if (!visible) {
      setReason('');
      setRecord(undefined as unknown as T);
      // 重置为默认配置
      setModalConfig({
        title: title,
        placeholder: placeholder
      });
    }
  }, [visible, title, placeholder]);

  return [
    (record, config) => {
      if (record) setRecord(record);
      // 如果传入了配置,则更新模态框配置
      if (config) {
        setModalConfig({
          title: config.title || title,
          placeholder: config.placeholder || placeholder
        });
      }
      setTrue();
    },
    <Modal
      visible={visible}
      title={modalConfig.title}
      onCancel={setFalse}
      onOk={() => {
        if (!reason) {
          message.error('请输入原因');
          return;
        }
        // 传入的onOk函数,可选链式调用,参数为输入框内容reason及行数据record
        onOk?.(reason, record).then(setFalse, log);
      }}
    >
      <Input.TextArea
        placeholder={modalConfig.placeholder}
        value={reason}
        onChange={(e) => {
          setReason(e.target.value);
        }}
      />
    </Modal>,
  ];
};

在改造后的版本中,useReasonModal 接受一个配置对象,包含 titleplaceholder 作为默认的模态框 title 和 placeholder。同时,useReasonModal返回的函数接受一个 record 和一个可选的 config 对象,允许在每次打开模态框时动态设置 title 和 placeholder。这下,代码的扩展性和复用都会达到很不错的效果,如果将来还要配置其他参数或者设计方式,也会容易得多。

使用方式如下:

js 复制代码
const [confirm, confirmReasonModal] = useReasonModal({
  title: '默认标题',
  placeholder: '默认占位符',
  onOk: async (reason, record) => {
    // 处理确认逻辑
  }
});

...

const columns = {
    ...,
    {
      title: '操作',
      render(_, record) {
        return (
          <>    
            <Button
              type="link"
              onClick={() => {
                // 显示模态框并传入自定义标题和占位符
                confirm(record, { title: '自定义标题', placeholder: '自定义占位符' });
              }}
            >
              确认按钮
            </Button>
          </>
        )
      },
    },
}

return (
    <>
      <Table columns={columns} />
      {confirmReasonModal}
    </>
)

总结

自定义 Hooks 是 React 生态中的一个强大工具,在我们日常开发中,可以使得逻辑复用和组件抽象变得更加简单。对于常见组件如何抽象出来组件化设计,也是我们开发中需要时刻思考和注意的。

本文对于 useReasonModal 的封装,展示了如何通过自定义 Hooks 来管理模态框的状态和事件处理,保持代码的干净和可维护。同时,useReasonModal的参数和类型设计使得它既通用又具有类型安全性。通过泛型,它可以适用于不同的数据结构,而参数设计则确保了它可以在不同的业务场景中灵活使用,具备更好的复用性和扩展性。

相关推荐
乐闻x4 分钟前
Vue.js 性能优化指南:掌握 keep-alive 的使用技巧
前端·vue.js·性能优化
一条晒干的咸魚6 分钟前
【Web前端】创建我的第一个 Web 表单
服务器·前端·javascript·json·对象·表单
花海少爷17 分钟前
第十章 JavaScript的应用课后习题
开发语言·javascript·ecmascript
Amd79421 分钟前
Nuxt.js 应用中的 webpack:compiled 事件钩子
前端·webpack·开发·编译·nuxt.js·事件·钩子
生椰拿铁You30 分钟前
09 —— Webpack搭建开发环境
前端·webpack·node.js
狸克先生41 分钟前
如何用AI写小说(二):Gradio 超简单的网页前端交互
前端·人工智能·chatgpt·交互
sinat_3842410943 分钟前
在有网络连接的机器上打包 electron 及其依赖项,在没有网络连接的机器上安装这些离线包
javascript·arcgis·electron
baiduopenmap1 小时前
百度世界2024精选公开课:基于地图智能体的导航出行AI应用创新实践
前端·人工智能·百度地图
loooseFish1 小时前
小程序webview我爱死你了 小程序webview和H5通讯
前端
小牛itbull1 小时前
ReactPress vs VuePress vs WordPress
开发语言·javascript·reactpress