简记 | 一个基于 AntD 的高效 useDrawer Hooks

在基于 Ant Design 的后台管理系统开发中,Drawer(抽屉)是一个非常高频使用的组件,常用于新增、编辑或展示详情。

如果你经常写类似下面这样的代码,你可能已经感到了厌倦:

tsx 复制代码
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
const [currentRow, setCurrentRow] = useState(null);

// ...一大堆处理 open、close、submit 的函数

每次都要重复管理 visibleloading 状态,还要手动写底部的"取消/确认"按钮逻辑。利用 React Hooks,我们来封装一个 useDrawer,彻底将抽屉的UI 逻辑业务逻辑解耦。

核心设计思路

我们需要一个 Hook,它能够:

  1. 自动管理显隐状态 :不需要在父组件手动 useState
  2. 便捷传参:打开抽屉时可以传入数据(例如编辑行数据)。
  3. 统一交互:自动生成底部的"取消"和"保存"按钮。
  4. 统一提交逻辑 :通过 Ref 调用子组件的 save 方法,自动处理 loading 状态。

代码实现

这是我们的 useDrawer 完整实现(TypeScript):

tsx 复制代码
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. 定义业务表单组件

子组件需要使用 forwardRefuseImperativeHandle 暴露一个 save 方法。这个方法通常包含表单验证和接口请求。

tsx 复制代码
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 状态。

tsx 复制代码
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>
  );
};

总结

  1. 逻辑解耦:父组件只负责"什么时候打开",子组件负责"具体内容"和"如何保存"。
  2. UI 统一 :所有抽屉都有统一的"取消/保存"按钮样式和位置。如果不需要,将 extra 的值设置为 <></> 即可。
  3. 极简调用 :通过 open(props) 直接传参,避免了在父组件再定义一个 currentEditItem 状态来暂存数据。
  4. Loading 自动托管save 方法是 Promise,Hook 会自动处理 Pending 期间的按钮 loading 状态,防止重复提交。
  5. 生命周期 :
    • Drawer 默认设置了 destroyOnHidden: true
    • 每次关闭 Drawer 时,内部组件会被销毁;每次打开时,内部组件会重新挂载。这确保了表单状态的重置。
相关推荐
2601_9495936520 分钟前
基础入门 React Native 鸿蒙跨平台开发:Animated 动画按钮组件 鸿蒙实战
react native·react.js·harmonyos
熊猫钓鱼>_>26 分钟前
动态网站发布部署核心问题详解
前端·nginx·容器化·网页开发·云服务器·静态部署
方也_arkling27 分钟前
elementPlus按需导入配置
前端·javascript·vue.js
我的xiaodoujiao41 分钟前
使用 Python 语言 从 0 到 1 搭建完整 Web UI自动化测试学习系列 44--将自动化测试结果自动推送至钉钉工作群聊
前端·python·测试工具·ui·pytest
沛沛老爹43 分钟前
Web开发者转型AI:多模态Agent视频分析技能开发实战
前端·人工智能·音视频
David凉宸1 小时前
vue2与vue3的差异在哪里?
前端·javascript·vue.js
笔画人生1 小时前
Cursor + 蓝耘API:用自然语言完成全栈项目开发
前端·后端
AC赳赳老秦1 小时前
外文文献精读:DeepSeek翻译并解析顶会论文核心技术要点
前端·flutter·zookeeper·自动化·rabbitmq·prometheus·deepseek
小宇的天下1 小时前
Calibre 3Dstack --每日一个命令day18【floating_trace】(3-18)
服务器·前端·数据库
毕设源码-钟学长1 小时前
【开题答辩全过程】以 基于web技术的酒店信息管理系统设计与实现-为例,包含答辩的问题和答案
前端