打造高灵活度动态表单:基于 React + Ant Design 的 useDynamicForm hooks 实现思路

在中后台系统开发中,表单是高频出现的交互组件。不同业务场景下,表单的字段、组件类型、校验规则往往差异很大,如果每个表单都重复编写渲染逻辑、校验逻辑和状态管理代码,会导致开发效率低下、代码冗余。本文将分享一个基于 React + Ant Design 实现的 useDynamicForm 自定义 hooks,帮助你快速构建可配置、高灵活度的动态表单。

一、核心特性

useDynamicForm hooks 封装了表单的核心能力,具备以下特性:

  • 高可配置性:通过 Schema 定义表单项的字段名、标签、组件类型、校验规则等

  • 支持多种组件:内置 Input、Select、Radio、Checkbox、DatePicker 等常用表单组件,支持扩展

  • 动态交互能力:支持根据表单值动态控制字段显示 / 隐藏、组件属性动态变化

  • 完整的 API 支持:提供表单值获取、设置、校验、重置、更新 Schema 等核心方法

  • 灵活的布局配置:支持单列、双列、三列布局,适配不同页面空间需求

  • 默认动作支持:内置提交 / 重置按钮,也可自定义实现

二、实现思路

1. 类型定义(type.ts)

首先通过 TypeScript 定义核心类型,确保类型安全和开发提示:

  • 定义表单组件类型 FormComponentType

  • 定义表单项配置 FormItemConfig,包含字段名、标签、组件类型、校验规则、动态属性等

  • 定义 hooks 配置 UseDynamicFormOptions,包含 Schema、布局配置、回调函数等

  • 定义暴露给外部的 API 接口 FormApi

2. 核心逻辑(useDynamicForm.tsx)

(1)组件渲染函数

renderComponent 函数根据表单项配置的 component 类型,渲染对应的 Ant Design 组件,并处理组件的动态属性(支持函数形式的属性配置,可获取表单值和设置值方法)。

(2)动态显示控制

shouldShowItem 函数根据表单项的 show 配置(函数形式),结合当前表单值,判断该字段是否需要显示。

(3)表单状态管理
  • 使用 Ant Design Form 的 useForm 管理表单实例

  • 通过状态 currentSchema 管理当前表单配置,支持动态更新

  • 通过 formValues 状态同步表单值,用于动态控制字段显示和组件属性

(4)API 暴露

通过 useImperativeHandle 向外部暴露表单核心操作方法,包括:

  • 获取 / 设置表单值(getValues/setValues/setFieldValue

  • 表单校验(validate/validateField

  • 表单重置(reset

  • 动态更新表单配置(updateFormSchema

3. 使用示例(MyForm.tsx)

通过 Schema 定义表单项,调用 useDynamicForm hooks 获取表单组件和 API,即可快速实现表单功能,支持自定义提交、重置、动态更新等操作。

三、完整代码实现

1. 类型定义文件(type.ts)

TypeScript 复制代码
import type { FormRule, FormInstance } from "antd";

// 定义所有可能的表单组件类型
export type FormComponentType =
  | "Input"
  | "Select"
  | "Radio"
  | "Checkbox"
  | "Switch"
  | "ProSelect"
  | "DatePicker"
  | "TextArea";

// 定义表单项的配置
export interface FormItemConfig {
  /** 字段名,对应表单值的 key */
  name: string;
  /** 表单项的标签 */
  label: string | null;
  /** 组件类型 */
  component: FormComponentType;
  /** 表单项的规则 */
  rules?: FormRule[];
  // 组件属性,支持函数形式(可获取表单值和设置值方法)
  componentProps?:
    | Record<string, any>
    | ((args: {
        formValues: Record<string, any>;
        setFieldValue: (name: string, value: any) => void;
        getFormValues: () => Record<string, any>;
      }) => Record<string, any>);
  // 是否禁用
  disabled?: boolean;
  // 占位符
  placeholder?: string;
  // 默认值
  defaultValue?: any;
  // 布局占比(对应 Ant Design Col 的 span)
  span?: number;
  // 条件显示配置:根据表单值判断是否显示
  show?: (values: Record<string, any>) => boolean;
}

export interface UseDynamicFormOptions {
  /** 表单项配置数组 */
  schema: FormItemConfig[];
  /** 是否显示默认的提交和重置按钮 */
  showDefaultActions?: boolean;
  /** 表单通用配置 */
  commonConfig?: {
    layout: "horizontal" | "vertical" | "inline";
    /** 标签后是否显示冒号 */
    colon?: boolean;
    labelCol?: object;
    wrapperCol?: object;
    column?: 1 | 2 | 3;
  };
  /** 标签布局配置 */
  labelCol?: object;
  /** 输入框布局配置 */
  wrapperCol?: object;
  /** 提交按钮的文本 */
  submitButtonText?: string;
  /** 重置按钮的文本 */
  resetButtonText?: string;
  /** 提交回调函数 */
  onSubmit?: (arg?: any) => void;
}

// 暴露给外部的表单 API
export interface FormApi {
  /** Ant Design 的 Form 实例,提供底层 API */
  formInstance: FormInstance;
  /** 获取所有字段值 */
  getValues: () => Record<string, any>;
  /** 设置字段值(批量) */
  setValues: (values: Record<string, any>) => void;
  /** 设置单个字段值 */
  setFieldValue: (name: string, value: any) => void;
  /** 校验表单 */
  validate: () => Promise<Record<string, any>>;
  /** 校验单个字段 */
  validateField: (name: string) => Promise<void>;
  /** 重置表单 */
  reset: (fields?: string[]) => void;
  /** 更新表单配置 */
  updateFormSchema: (schema: FormItemConfig[]) => void;
  /** 获取单个字段值 */
  getFieldValue: (name: string) => any;
  /** 获取最新表单值 */
  getFormValues: () => Record<string, any>;
  /** 判断表单是否有修改 */
  isDirty: () => boolean;
}

2. hooks 实现文件(useDynamicForm.tsx)

TypeScript 复制代码
import { Button } from "antd";
import { getSelectData } from "@/utils/common";
import { HEALTHRISKASSESSMENT_RISK } from "@/concacts/valueEnum";
import { useDynamicForm } from "@/hooks/DynamicForm/useDynamicForm";
import type { FormItemConfig } from "@/hooks/DynamicForm/type";
import { SmileOutlined } from '@ant-design/icons';

function MyForm() {
    const smileIcon = <SmileOutlined />;

    // 1. 定义表单 Schema
    const loginSchema: FormItemConfig[] = [
        {
            name: "username",
            label: "用户名",
            component: "Input",
            placeholder: "请输入用户名",
            rules: [{ required: true, message: "请输入用户名!" }],
        },
        {
            name: "phone",
            label: "手机号",
            component: "Input",
            placeholder: "请输入手机号",
            rules: [
                { required: true, message: "请输入手机号!" },
                { pattern: /^1[3-9]\d{9}$/, message: "请输入正确的手机号格式!" },
            ],
        },
        {
            name: "price",
            label: "金额单位",
            component: "Select",
            placeholder: "请选择金额单位",
            rules: [{ required: true, message: "请选择金额单位!" }],
            componentProps: {
                options: [
                    { value: "CNY", label: "人民币" },
                    { value: "USD", label: "美元" },
                    { value: "EUR", label: "欧元" },
                ],
                showSearch: true,
                onChange: (val, option) => {
                    console.log('金额单位变更:', val, option);
                },
            },
        },
        {
            name: "furit",
            label: "水果",
            component: "Checkbox",
            placeholder: "请选择水果",
            rules: [{ required: true, message: "请选择水果!" }],
            componentProps({ formValues, setFieldValue, getFormValues }) {
                return {
                    options: [
                        { label: "苹果", value: "Apple" },
                        { label: "梨子", value: "Pear", disabled: true },
                        { label: "橙子", value: "Orange" },
                    ],
                    onChange: (val) => {
                        // 选择苹果时,自动设置风险等级和用户名、备注
                        if (val.includes('Apple')) {
                            setFieldValue('sickness', ['1']);
                            setFieldValue('username', `用户_${Date.now().toString().slice(-6)}`);

                            // 获取最新的表单值
                            const latestValues = getFormValues();
                            console.log('最新风险等级:', latestValues.sickness);

                            if (latestValues.sickness && latestValues.sickness.includes('1')) {
                                setFieldValue('remark', '您选择了苹果,已自动填写备注');
                            }
                        } else {
                            // 未选择苹果时,清空相关字段
                            setFieldValue('sickness', undefined);
                            setFieldValue('remark', undefined);
                            setFieldValue('username', `用户_${Date.now().toString().slice(-6)}`);
                        }
                    },
                }
            },
        },
        {
            name: "sickness",
            label: "风险等级",
            component: "ProSelect",
            placeholder: "请选择风险等级",
            rules: [{ required: true, message: "请选择风险等级!" }],
            componentProps({ setFieldValue }) {
                return {
                    // 正常传递Select的文档options即可,这里无须复制
                    options: getSelectData(HEALTHRISKASSESSMENT_RISK),
                    showSearch: true,
                    onChange: (val) => {
                        console.log('风险等级变更:', val);
                        if (val.length === 0) {
                            setFieldValue('remark', undefined);
                        }
                    },
                    mode: "multiple",
                }
            }
        },
        {
            name: "birthDate",
            label: "出生日期",
            component: "DatePicker",
            placeholder: "请选择出生日期",
            rules: [{ required: true, message: "请选择出生日期!" }],
            componentProps: {
                onChange: (val) => {
                    console.log('出生日期变更:', val);
                },
                disabledDate: (current) => {
                    // 禁用过去的日期
                    return current && current < new Date();
                },
                prefix: smileIcon,
                style: { backgroundColor: 'lightgreen' }
            }
        },
        {
            name: "remark",
            label: "备注",
            component: "TextArea",
            placeholder: "请输入备注",
            rules: [{ required: true, message: "请输入备注!" }],
            span: 24, // 占满一行
            show(values) {
                // 风险等级有选择时才显示备注字段
                return values.sickness && values.sickness.length > 0;
            },
        },
    ];

    // 2. 调用 hooks,获取表单组件和 API
    const [Form, formApi] = useDynamicForm({
        schema: loginSchema,
        commonConfig: {
            layout: "horizontal",
            colon: true,
            column: 2, // 双列布局
        },
        onSubmit: (val) => {
            console.log('表单提交(默认按钮):', val);
        },
    });

    // 自定义提交逻辑
    const handleSubmit = async () => {
        try {
            // 表单校验
            await formApi.validate();
            // 获取表单值
            const values = formApi.getValues();
            console.log('表单提交(自定义按钮):', values);
            // 这里可以添加接口请求等业务逻辑
        } catch (error) {
            console.log('表单校验失败:', error);
        }
    };

    // 手动设置表单值(回显场景)
    const setValues = () => {
        formApi.setValues({
            sickness: ['1'], // 风险等级设为第一个选项
        });
    };

    // 动态更新表单配置
    const updateFormSchema = () => {
        formApi.updateFormSchema([
            {
                name: "username",
                label: "用户名",
                component: "Input",
                placeholder: "请输入用户名",
                rules: [{ required: true, message: "请输入用户名!" }],
            },
            {
                name: "phone",
                label: "手机号",
                component: "Input",
                placeholder: "请输入手机号",
                rules: [{ required: true, message: "请输入手机号!" }],
            },
        ]);
    };

    return (
        <>
            <div className="w-1/2">
                {/* 渲染表单 */}
                <Form />
            </div>

            {/* 自定义操作按钮 */}
            <div className="flex mt-4">
                <Button className="mr-4" type="primary" onClick={handleSubmit}>
                    自定义提交
                </Button>
                <Button className="mr-4" onClick={() => formApi.reset()}>
                    重置表单
                </Button>
                <Button className="mr-4" type="dashed" onClick={updateFormSchema}>
                    动态更新表单
                </Button>
                <Button type="dashed" onClick={setValues}>
                    手动设置值
                </Button>
            </div>
        </>
    );
}

export default MyForm;

3. 使用示例(MyForm.tsx)

TypeScript 复制代码
import { Button } from "antd";
import { getSelectData } from "@/utils/common";
import { HEALTHRISKASSESSMENT_RISK } from "@/concacts/valueEnum";
import { useDynamicForm } from "@/hooks/DynamicForm/useDynamicForm";
import type { FormItemConfig } from "@/hooks/DynamicForm/type";
import { SmileOutlined } from '@ant-design/icons';

function MyForm() {
    const smileIcon = <SmileOutlined />;

    // 1. 定义表单 Schema
    const loginSchema: FormItemConfig[] = [
        {
            name: "username",
            label: "用户名",
            component: "Input",
            placeholder: "请输入用户名",
            rules: [{ required: true, message: "请输入用户名!" }],
        },
        {
            name: "phone",
            label: "手机号",
            component: "Input",
            placeholder: "请输入手机号",
            rules: [
                { required: true, message: "请输入手机号!" },
                { pattern: /^1[3-9]\d{9}$/, message: "请输入正确的手机号格式!" },
            ],
        },
        {
            name: "price",
            label: "金额单位",
            component: "Select",
            placeholder: "请选择金额单位",
            rules: [{ required: true, message: "请选择金额单位!" }],
            componentProps: {
                options: [
                    { value: "CNY", label: "人民币" },
                    { value: "USD", label: "美元" },
                    { value: "EUR", label: "欧元" },
                ],
                showSearch: true,
                onChange: (val, option) => {
                    console.log('金额单位变更:', val, option);
                },
            },
        },
        {
            name: "furit",
            label: "水果",
            component: "Checkbox",
            placeholder: "请选择水果",
            rules: [{ required: true, message: "请选择水果!" }],
            componentProps({ formValues, setFieldValue, getFormValues }) {
                return {
                    options: [
                        { label: "苹果", value: "Apple" },
                        { label: "梨子", value: "Pear", disabled: true },
                        { label: "橙子", value: "Orange" },
                    ],
                    onChange: (val) => {
                        // 选择苹果时,自动设置风险等级和用户名、备注
                        if (val.includes('Apple')) {
                            setFieldValue('sickness', ['1']);
                            setFieldValue('username', `用户_${Date.now().toString().slice(-6)}`);

                            // 获取最新的表单值
                            const latestValues = getFormValues();
                            console.log('最新风险等级:', latestValues.sickness);

                            if (latestValues.sickness && latestValues.sickness.includes('1')) {
                                setFieldValue('remark', '您选择了苹果,已自动填写备注');
                            }
                        } else {
                            // 未选择苹果时,清空相关字段
                            setFieldValue('sickness', undefined);
                            setFieldValue('remark', undefined);
                            setFieldValue('username', `用户_${Date.now().toString().slice(-6)}`);
                        }
                    },
                }
            },
        },
        {
            name: "sickness",
            label: "风险等级",
            component: "ProSelect",
            placeholder: "请选择风险等级",
            rules: [{ required: true, message: "请选择风险等级!" }],
            componentProps({ setFieldValue }) {
                return {
                    options: getSelectData(HEALTHRISKASSESSMENT_RISK),
                    showSearch: true,
                    onChange: (val) => {
                        console.log('风险等级变更:', val);
                        if (val.length === 0) {
                            setFieldValue('remark', undefined);
                        }
                    },
                    mode: "multiple",
                }
            }
        },
        {
            name: "birthDate",
            label: "出生日期",
            component: "DatePicker",
            placeholder: "请选择出生日期",
            rules: [{ required: true, message: "请选择出生日期!" }],
            componentProps: {
                onChange: (val) => {
                    console.log('出生日期变更:', val);
                },
                disabledDate: (current) => {
                    // 禁用过去的日期
                    return current && current < new Date();
                },
                prefix: smileIcon,
                style: { backgroundColor: 'lightgreen' }
            }
        },
        {
            name: "remark",
            label: "备注",
            component: "TextArea",
            placeholder: "请输入备注",
            rules: [{ required: true, message: "请输入备注!" }],
            span: 24, // 占满一行
            show(values) {
                // 风险等级有选择时才显示备注字段
                return values.sickness && values.sickness.length > 0;
            },
        },
    ];

    // 2. 调用 hooks,获取表单组件和 API
    const [Form, formApi] = useDynamicForm({
        schema: loginSchema,
        commonConfig: {
            layout: "horizontal",
            colon: true,
            column: 2, // 双列布局
        },
        onSubmit: (val) => {
            console.log('表单提交(默认按钮):', val);
        },
    });

    // 自定义提交逻辑
    const handleSubmit = async () => {
        try {
            // 表单校验
            await formApi.validate();
            // 获取表单值
            const values = formApi.getValues();
            console.log('表单提交(自定义按钮):', values);
            // 这里可以添加接口请求等业务逻辑
        } catch (error) {
            console.log('表单校验失败:', error);
        }
    };

    // 手动设置表单值(回显场景)
    const setValues = () => {
        formApi.setValues({
            sickness: ['1'], // 风险等级设为第一个选项
        });
    };

    // 动态更新表单配置
    const updateFormSchema = () => {
        formApi.updateFormSchema([
            {
                name: "username",
                label: "用户名",
                component: "Input",
                placeholder: "请输入用户名",
                rules: [{ required: true, message: "请输入用户名!" }],
            },
            {
                name: "phone",
                label: "手机号",
                component: "Input",
                placeholder: "请输入手机号",
                rules: [{ required: true, message: "请输入手机号!" }],
            },
        ]);
    };

    return (
        <>
            <div className="w-1/2">
                {/* 渲染表单 */}
                <Form />
            </div>

            {/* 自定义操作按钮 */}
            <div className="flex mt-4">
                <Button className="mr-4" type="primary" onClick={handleSubmit}>
                    自定义提交
                </Button>
                <Button className="mr-4" onClick={() => formApi.reset()}>
                    重置表单
                </Button>
                <Button className="mr-4" type="dashed" onClick={updateFormSchema}>
                    动态更新表单
                </Button>
                <Button type="dashed" onClick={setValues}>
                    手动设置值
                </Button>
            </div>
        </>
    );
}

export default MyForm;

四、关键功能说明

1. 动态表单配置

通过 FormItemConfig 数组定义表单结构,支持以下配置项:

  • name:字段唯一标识(对应表单值的 key)

  • label:字段标签

  • component:组件类型(如 Input、Select 等)

  • rules:校验规则(Ant Design FormRule 格式)

  • componentProps:组件属性(支持静态对象或动态函数)

  • show:条件显示函数(根据表单值判断是否显示)

  • defaultValue:默认值

  • span:布局占比

2. 动态交互能力

(1)组件属性动态变化

componentProps 支持函数形式,可获取以下参数:

  • formValues:当前表单值(状态同步值)

  • setFieldValue:设置单个字段值的方法

  • getFormValues:获取最新表单值的方法(从表单实例中获取)

示例:选择水果时,自动设置风险等级、用户名和备注。

(2)字段条件显示

通过 show 配置项,传入一个函数,根据当前表单值判断字段是否显示。

示例:只有选择了风险等级,才显示备注字段。

3. 表单 API 用法

(1)获取表单值
TypeScript 复制代码
const values = formApi.getValues(); // 获取所有字段值
const username = formApi.getFieldValue('username'); // 获取单个字段值
(2)设置表单值
TypeScript 复制代码
formApi.setValues({ username: '张三', phone: '13800138000' }); // 批量设置
formApi.setFieldValue('username', '张三'); // 单个设置
(3)表单校验
TypeScript 复制代码
try {
    await formApi.validate(); // 校验所有字段
    // 校验通过,处理提交逻辑
} catch (error) {
    // 校验失败,处理错误
}

// 校验单个字段
await formApi.validateField('phone');
(4)重置表单
TypeScript 复制代码
formApi.reset(); // 重置所有字段
formApi.reset(['username', 'phone']); // 重置指定字段
(5)动态更新表单配置
TypeScript 复制代码
formApi.updateFormSchema(newSchema); // 传入新的 FormItemConfig 数组

五、扩展与优化建议

1. 组件扩展

如果需要支持更多组件(如 InputNumberUpload 等),只需:

  • FormComponentType 中添加组件类型

  • renderComponent 函数中添加对应的组件渲染逻辑

  • 支持自定义组件,如上述示例ProSelect

TypeScript 复制代码
import { Select } from "antd";
import { ProSelectProps } from "./prop";

function ProSelect(props: ProSelectProps) {
  const {
    placeholder = "请选择",
    showSearch = false,
    allowClear = true,
    mode,
    onChange,
    ...selectProps
  } = props;

  const handleChange = (value: any, options: any) => {
    // if (callback) {
    //   callback(value, options);
    // }
    onChange(value,options);
  };

  return (
    
      <Select
        placeholder={placeholder}
        allowClear={allowClear}
        showSearch={showSearch}
        optionFilterProp="label"
   
        onChange={handleChange}
        {...selectProps}
             mode={mode}
      />

  );
}

export default ProSelect;

六、总结

useDynamicForm hooks 通过 Schema 配置化的方式,封装了表单的渲染、状态管理、校验等核心逻辑。

相关推荐
用户600071819101 小时前
【翻译】我们如何打造v0版iOS应用
前端
Sahadev_1 小时前
做APP开发有哪些基建可以做?完整调试工具集实践指南(以ReactNative为例)
javascript·react native·react.js
阿民不加班1 小时前
【React】使用browser-image-compression在上传前压缩图片、react上传图片压缩
前端·javascript·react.js
前端_yu小白1 小时前
前端实现录音,获取流分析音量大小,设置相应的动画
前端·mediarecorder·录音·浏览器安全性检查·https部署
虎子_layor1 小时前
小程序登录到底是怎么工作的?一次请求背后的三方信任链
前端·后端
草字1 小时前
css 父节点设置display: flex; align-items: center;,子节点如何跟随其他子节点撑高的高度
前端·javascript·css
我命由我123451 小时前
微信小程序 - 页面跳转并传递参数(使用路由参数、使用全局变量、使用本地存储、使用路由参数结合本地存储)
开发语言·前端·javascript·微信小程序·小程序·前端框架·js
DJ斯特拉1 小时前
日志技术Logback
java·前端·logback
HIT_Weston1 小时前
49、【Ubuntu】【Gitlab】拉出内网 Web 服务:http.server 单/多线程分析(一)
前端·ubuntu·gitlab