简记 | 一个基于 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 的函数

每次都要重复管理 visible、loading 状态,还要手动写底部的"取消/确认"按钮逻辑。利用 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. 定义业务表单组件

子组件需要使用 forwardRef 和 useImperativeHandle 暴露一个 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 时,内部组件会被销毁;每次打开时,内部组件会重新挂载。这确保了表单状态的重置。
相关推荐
子兮曰5 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰5 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万5 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝5 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋5 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁5 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王95276 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大6 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师6 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端
沙蒿同学6 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端