简记 | 一个基于 AntD 的高效 useDrawer Hooks
2026年1月11日 12:50
在基于 Ant Design 的后台管理系统开发中,Drawer(抽屉)是一个非常高频使用的组件,常用于新增、编辑或展示详情。
如果你经常写类似下面这样的代码,你可能已经感到了厌倦:
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
const [currentRow, setCurrentRow] = useState(null);
// ...一大堆处理 open、close、submit 的函数
每次都要重复管理 visible、loading 状态,还要手动写底部的“取消/确认”按钮逻辑。利用 React Hooks,我们来封装一个 useDrawer,彻底将抽屉的UI 逻辑与业务逻辑解耦。
核心设计思路
我们需要一个 Hook,它能够:
-
自动管理显隐状态:不需要在父组件手动
useState。 - 便捷传参:打开抽屉时可以传入数据(例如编辑行数据)。
- 统一交互:自动生成底部的“取消”和“保存”按钮。
-
统一提交逻辑:通过 Ref 调用子组件的
save方法,自动处理 loading 状态。
代码实现
这是我们的 useDrawer 完整实现(TypeScript):
import React, { useState, useCallback, useRef, useMemo } from 'react';
import { Drawer, Space, Button } from 'antd';
import type { DrawerProps } from 'antd';
// 约定子组件必须暴露 save 方法
interface ComponentRef {
save: () => Promise<void>;
}
export function useDrawer<T extends object>(
Component: React.ComponentType<T>,
drawerProps: Omit<DrawerProps, 'open' | 'onClose'>
) {
const componentRef = useRef<ComponentRef>(null);
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [componentProps, setComponentProps] = useState<T | null>(null);
// 打开抽屉,支持传入初始 Props
const show = useCallback((props?: T) => {
setComponentProps(props ?? null);
setOpen(true);
}, []);
const close = useCallback(() => {
setOpen(false);
setComponentProps(null);
}, []);
const DrawerHolder = useMemo(() => {
if (!open) return null;
return (
<Drawer
open={open}
onClose={close}
destroyOnHidden
extra={
<Space>
<Button onClick={close}>取消</Button>
<Button
type="primary"
loading={loading}
onClick={async () => {
try {
setLoading(true);
// 核心:调用子组件的 save 方法
await componentRef.current?.save();
close();
} catch (err) {
console.error('保存失败', err);
} finally {
setLoading(false);
}
}}
>
保存
</Button>
</Space>
}
{...drawerProps}
>
{/* 将 ref 和 props 传递给业务组件 */}
<Component ref={componentRef} {...(componentProps as T)} />
</Drawer>
);
}, [open, close, drawerProps, loading, Component, componentProps]);
return {
open: show,
close,
DrawerHolder
};
}
如何使用?
使用这个 Hook 分为两步:定义表单组件、在父组件调用。
1. 定义业务表单组件
子组件需要使用 forwardRef 和 useImperativeHandle 暴露一个 save 方法。这个方法通常包含表单验证和接口请求。
import { forwardRef, useImperativeHandle } from 'react';
import { Form, Input, message } from 'antd';
// UserForm.tsx
const UserForm = forwardRef((props: { id?: string; name?: string }, ref) => {
const [form] = Form.useForm();
useImperativeHandle(ref, () => ({
save: async () => {
// 1. 触发表单验证
const values = await form.validateFields();
// 2. 模拟接口请求
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('提交数据:', { ...props, ...values });
message.success('保存成功');
}
}));
return (
<Form form={form} initialValues={props} layout="vertical">
<Form.Item name="name" Label="用户名称" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Form>
);
});
export default UserForm;
2. 在父组件中调用
在父组件中,我们只需要像调用函数一样打开抽屉,完全不需要关心 visible 状态和 loading 状态。
import { Button } from 'antd';
import { useDrawer } from './hooks/useDrawer';
import UserForm from './UserForm';
const UserList = () => {
// 初始化 Hook,传入组件配置
const { open, DrawerHolder } = useDrawer(UserForm, {
title: '编辑用户',
width: 600
});
const handleEdit = (record) => {
// 一行代码唤起抽屉,并透传数据
open({ id: record.id, name: record.name });
};
return (
<div>
<Button type="primary" onClick={() => open()}>新增用户</Button>
<Button onClick={() => handleEdit({ id: '1', name: 'John' })}>编辑用户</Button>
{/* 渲染抽屉占位符 */}
{DrawerHolder}
</div>
);
};
总结
- 逻辑解耦:父组件只负责“什么时候打开”,子组件负责“具体内容”和“如何保存”。
-
UI 统一:所有抽屉都有统一的“取消/保存”按钮样式和位置。如果不需要,将
extra的值设置为<></>即可。 -
极简调用:通过
open(props)直接传参,避免了在父组件再定义一个currentEditItem状态来暂存数据。 -
Loading 自动托管:
save方法是 Promise,Hook 会自动处理 Pending 期间的按钮 loading 状态,防止重复提交。 -
生命周期:
- Drawer 默认设置了
destroyOnHidden: true。 - 每次关闭 Drawer 时,内部组件会被销毁;每次打开时,内部组件会重新挂载。这确保了表单状态的重置。
- Drawer 默认设置了