背景
在大型项目的开发中,很多地方都需要弹出理由/原因的输入框弹窗,比如用户取消订单时输入取消原因,或者后台拉黑时写明拉黑理由。每次都要写一堆相似的代码来控制弹窗的显示、隐藏和处理用户输入,一旦面临改动,就得到处找代码去修改,非常之麻烦。
出于组件化考虑,大家肯定会在项目中封装一个类似于 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
是一个独立的组件,它可以接收 title
、visible
、onOk
、onCancel
作为属性。父组件 ParentComponent
控制模态框的打开和关闭,并处理提交输入内容的逻辑。
在 JSX组件
的封装中,ReasonModal
组件是一个独立的 React 组件,它直接接收父组件传递的 props 来管理状态和逻辑。它的缺点也在于此,每次使用时都需要在父组件中重复状态管理和逻辑处理的代码,必然会增加代码量和维护成本。
同时,假设我们的应用场景是在一个列表中的某栏中的操作属性,当我们在上传输入框中的内容时,同时需要参考到本行数据的值 record。为了将 record 中的值传给模态框组件,那么在父组件中就又需要多管理一个状态值,并在函数 handleOpenModal()
中去改变状态值,并在 ReasonModal
的 props
中添加传递的 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
类型编写特定的代码,进而提高了代码的复用性和类型安全性。
其他注意
onOk
最好返回一个Promise,结果reject时弹窗不会关闭 结果resolve时会自动关闭- 优雅地管理模态框显隐的布尔值状态 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
接受一个配置对象,包含 title
和 placeholder
作为默认的模态框 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
的参数和类型设计使得它既通用又具有类型安全性。通过泛型,它可以适用于不同的数据结构,而参数设计则确保了它可以在不同的业务场景中灵活使用,具备更好的复用性和扩展性。