普通视图

发现新文章,点击刷新页面。
昨天以前首页

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

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

核心设计思路

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

  1. 自动管理显隐状态:不需要在父组件手动 useState
  2. 便捷传参:打开抽屉时可以传入数据(例如编辑行数据)。
  3. 统一交互:自动生成底部的“取消”和“保存”按钮。
  4. 统一提交逻辑:通过 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. 定义业务表单组件

子组件需要使用 forwardRefuseImperativeHandle 暴露一个 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>
  );
};

总结

  1. 逻辑解耦:父组件只负责“什么时候打开”,子组件负责“具体内容”和“如何保存”。
  2. UI 统一:所有抽屉都有统一的“取消/保存”按钮样式和位置。如果不需要,将 extra 的值设置为 <></> 即可。
  3. 极简调用:通过 open(props) 直接传参,避免了在父组件再定义一个 currentEditItem 状态来暂存数据。
  4. Loading 自动托管save 方法是 Promise,Hook 会自动处理 Pending 期间的按钮 loading 状态,防止重复提交。
  5. 生命周期:
    • Drawer 默认设置了 destroyOnHidden: true
    • 每次关闭 Drawer 时,内部组件会被销毁;每次打开时,内部组件会重新挂载。这确保了表单状态的重置。
❌
❌