🚀 表单联动方案设计

749 阅读4分钟

当表单里的字段开始互相“使眼色”,你的代码还能撑住吗?
这位同学,你还在为表单联动而疯狂堆 useEffect 吗?
今天,我们就来盘点几种搞定复杂联动的方案,总有一款适合你!

📋数据“样板间”:一个会搞事情的表单

先来看一个经典场景 👀
任务信息收集表单,字段如下:

  • 任务名称 (taskName) —— 普通输入框
  • 优先级 (priority) —— 下拉单选
  • 所属部门 (department) —— 下拉单选
  • 负责人 (owner) —— 下拉单选(随部门 department 变化
  • 节点 (nodes) —— 下拉多选(随负责人 owner 变化

联动规则:

  1. 切换部门 → 负责人选项更新
  2. 切换负责人 → 节点选项更新

这里只描述了选项间的联动,还会有当前值、显隐、禁用态等的联动设置。

听起来不难?但当你需要面对几十个甚至上百个字段,互相牵一发而动全身时,代码就会变成一锅粥

表单配置

  1. 表单所有字段配置项
export const formFields: Record<string, FormFieldConfig> = {
    'taskName': {
        name: "taskName",
        label: "任务名称",
        type: "input"
    },
    'priority': {
        name: "priority",
        label: "优先级",
        type: "select",
        options: [
            { label: "P0", value: "P0" },
            { label: "P1", value: "P1" },
            { label: "P2", value: "P2" }
        ]
    },
    'department': {
        name: "department",
        label: "所属部门",
        type: "select",
        options: [
            { label: "研发部", value: "dev" },
            { label: "产品部", value: "product" },
            { label: "运营部", value: "ops" }
        ]
    },
    'owner': {
        name: "owner",
        label: "负责人",
        type: "select",
        dependencies: ["department"]
    },
    'nodes': {
        name: "nodes",
        label: "节点",
        type: "multiSelect",
        dependencies: ["owner"]
    }
};
  1. 表单初始值
export const initialValues = {
    taskName: "",
    priority: "P0",
    department: undefined,
    owner: undefined,
    nodes: []
};
  1. 模拟不同的选项值
export const ownerMap = {
    dev: [
        { label: "张三", value: "zhangsan" },
        { label: "李四", value: "lisi" }
    ],
    product: [
        { label: "王五", value: "wangwu" }
    ],
    ops: [
        { label: "赵六", value: "zhaoliu" }
    ]
};
export const nodeMap = {
    zhangsan: [
        { label: "开发", value: "dev" },
        { label: "测试", value: "test" }
    ],
    lisi: [
        { label: "开发", value: "dev" }
    ],
    wangwu: [
        { label: "需求评审", value: "review" }
    ],
    zhaoliu: [
        { label: "运营推广", value: "ops" }
    ]
};
  1. 表单控件匹配组件
interface FieldRendererProps extends React.ComponentProps<any> {
    config: FormFieldConfig;
}

export const FieldRenderer: React.FC<FieldRendererProps> = ({config,...restProps}) => {
    const { type, options } = config;
    switch (type) {
        case "input":
        return <Input {...restProps} />;
        
        case "number":
        return <InputNumber {...restProps} style={{ width: "100%" }} />;
    
        case "select":
        return <Select {...restProps} options={options} />;
    
        case "multiSelect":
        return <Select {...restProps} mode="multiple" options={options}/>;
        
        default:
        return null;
    }
};

方案一:🔧 AntD 原生 dependencies

思路:直接用 Antd Form 的 dependencies + getFieldValue

const TaskForm = () => {
    const [form] = Form.useForm(); // form实例

    return (
        <Form
            form={form}
            initialValues={initialValues}
            layout="vertical"
        >
            <Form.Item name="taskName" label="任务名称">
                <FieldRenderer config={formFields['taskName']} />
            </Form.Item>
            <Form.Item name="priority" label="优先级">
                <FieldRenderer config={formFields['priority']} />
            </Form.Item>
            <Form.Item name="department" label="所属部门">
                <FieldRenderer config={formFields['department']} />
            </Form.Item>
            <Form.Item noStyle dependencies={["department"]}>
                {({ getFieldValue }) => {
                    const dep: keyof typeof ownerMap = getFieldValue("department") ;
                    return (
                        <Form.Item name="owner" label="负责人">
                            <Select options={ownerMap[dep] || []} />
                        </Form.Item>
                    );
                }}
            </Form.Item>
            <Form.Item noStyle dependencies={["owner"]}>
                {({ getFieldValue }) => {
                    const owner: keyof typeof nodeMap = getFieldValue("owner");
                    return (
                        <Form.Item name="nodes" label="节点">
                            <Select mode="multiple" options={nodeMap[owner] || []}/>
                        </Form.Item>
                    );
                }}
            </Form.Item>
        </Form>
    );
};
image.png **👍 优点** - 简单直接,适合小型表单

👎 缺点

  • 逻辑分散,不要适合多对多;难以复用,换个表单就得重写一遍。

方案二:📋 onValuesChange + 依赖映射表

思路:抽离联动规则到配置表,在 onValuesChange 里统一执行。

const TaskForm2 = () => {
  const [form] = Form.useForm();
  
  const [owners, setOwners] = useState<{label: string; value: string;}[]>([]);
  const [nodes, setNodes] = useState<{label: string; value: string;}[]>([]);

  // 处理联动逻辑
  const handleValuesChange = (changed) => {
    if (changed.department) {
      const list = ownerMap[changed.department as keyof typeof ownerMap] || [];
      setOwners(list);

      form.setFieldsValue({
        owner: undefined,
        nodes: [],
      });
    }

    if (changed.owner) {
      const list = nodeMap[changed.owner as keyof typeof nodeMap] || [];
      setNodes(list);

      form.setFieldsValue({
        nodes: [],
      });
    }
  };

  return (
    <Form
      form={form}
      layout="vertical"
      onValuesChange={handleValuesChange}
      initialValues={initialValues}
    >
      <Form.Item name="taskName" label="任务名称">
        <FieldRenderer config={formFields["taskName"]} />
      </Form.Item>

      <Form.Item name="priority" label="优先级">
        <FieldRenderer config={formFields["priority"]} />
      </Form.Item>

      <Form.Item name="department" label="所属部门">
        <FieldRenderer config={formFields["department"]} />
      </Form.Item>

      <Form.Item name="owner" label="负责人">
        <Select options={owners} />
      </Form.Item>

      <Form.Item name="nodes" label="节点">
        <Select mode="multiple" options={nodes} />
      </Form.Item>
    </Form>
  );
};
image.png **👍 优点** - 规则集中管理,一眼看清所有联动。 - 容易扩展新规则,维护性提升。

👎 缺点

  • 异步处理需要额外状态(如 loading)。
  • 仍需手动触发 setFieldsValue,不够“响应式”。
  • 联动过于复杂时,onValuesChange函数行数会超出限制,无法控制

方案三:🎣 onValuesChange + 自定义 Hook

思路:封装 useFormLinkage,内部维护依赖状态,处理valueChange逻辑。

export const useFormLinkage = () => {
  const [owners, setOwners] = useState<{
      label: string;
      value: string;
    }[]>([]);
  const [nodes, setNodes] = useState<{
      label: string;
      value: string;
    }[]>([]);

  const handleChange = (changed: any) => {
    if (changed.department) {
      setOwners(ownerMap[changed.department as keyof typeof ownerMap] || []);
    }

    if (changed.owner) {
      setNodes(nodeMap[changed.owner as keyof typeof nodeMap] || []);
    }
  };

  return {
    owners,
    nodes,
    handleChange
  };
};
const TaskForm3 = () => {
  const [form] = Form.useForm();

  const { owners, nodes, handleChange } = useFormLinkage();

  return (
    <Form
      form={form}
      layout="vertical"
      onValuesChange={handleChange}
      initialValues={initialValues}
    >
      <Form.Item name="taskName" label="任务名称">
        <FieldRenderer config={formFields["taskName"]} />
      </Form.Item>

      <Form.Item name="priority" label="优先级">
        <FieldRenderer config={formFields["priority"]} />
      </Form.Item>

      <Form.Item name="department" label="所属部门">
        <FieldRenderer config={formFields["department"]} />
      </Form.Item>

      <Form.Item name="owner" label="负责人">
        <Select options={owners} />
      </Form.Item>

      <Form.Item name="nodes" label="节点">
        <Select mode="multiple" options={nodes} />
      </Form.Item>
    </Form>
  );
};
image.png **👍 优点** - 联动逻辑与 UI 完全解耦,配置可复用。

👎 缺点

  • 需要自己管理状态和依赖。
  • 会创建多个变量。

方案四:🚀 Formily

思路:采用阿里巴巴开源的 Formily,用 JSON Schema + 响应式模型声明联动。

{
  "type": "object",
  "properties": {
    "department": { "type": "string", "enum": [...] },
    "owner": {
      "type": "string",
      "x-reactions": [
        {
          "dependencies": ["department"],
          "fulfill": {
            "state": { "disabled": "{{!$deps[0]}}" },
            "schema": { "enum": "{{$form.getFieldState('department', s => fetchOwnerOptions(s.value))}}" }
          }
        }
      ]
    },
    "priority": { "type": "number" },
    "nodes": {
      "type": "array",
      "x-reactions": [
        {
          "dependencies": ["priority"],
          "fulfill": { "schema": { "enum": "{{$deps[0] > 5 ? urgentNodes : allNodes}}" } }
        }
      ]
    }
  }
}

👍 优点

  • 功能极其强大,支持复杂联动、异步、校验、布局。
  • 开箱即用,社区成熟,有完整生态。

👎 缺点

  • 学习曲线陡峭,需要理解 Formily 的响应式模型。
  • 引入较大依赖,可能“杀鸡用牛刀”。

方案五:🔄 FieldLifecycleCallbacks 生命周期回调

思路:为每个字段配置生命周期回调(如 afterValueChange),字段值变化时自动调用注册的函数。

1️⃣ lifecycleCallbacks

每个字段都有一个lifecycleCallbacks生命周期回调集合,类似于React中的Class组件,在不同阶段执行不同的事情。

集合中可以包含以下回调事件:

  • initFieldValue:初始化字段值
  • beforeFieldValueChange:更改字段值前
  • afterFieldValueChange:更改字段值后
private lifecycleCallbacks: IFieldLifecycleCallbacks = {}; // 字段特定生命周期阶段触发的回调函数集合

2️⃣ afterFieldValueChange

afterFieldValueChange 事件回调中接受以下三个参数:

export type AfterFieldValueChange = (params: {
    changedKey: string; // 当前被更改的字段 key
    changedValue: any; // 字段更改后的新值
    field: IField; // 受影响的字段实例
}) => void;

例如负责人可选项受部门的影响,那ownerlifecycleCallbacks就可以增加一个afterFieldValueChange,节点同理:

owner: {
  afterFieldValueChange: ({
    changedKey,
    changedValue: value,
    field: { clearSelfValue },
  }) => {
    if (changedKey === "department") {
      const list = ownerMap[value as keyof typeof ownerMap] || [];
      setOwners(list);
      clearSelfValue();
    }
  },
},
nodes: {
  afterFieldValueChange: ({
    changedKey,
    changedValue: value,
    field: { clearSelfValue },
  }) => {
    if (changedKey === "owner") {
      const list = nodeMap[value as keyof typeof nodeMap] || [];
      setNodes(list);
      clearSelfValue();
    }
  },
},

3️⃣ FormModel + FieldModel

参考之前的几种方式,可以知道Form表单中可以感知变化的就是onValuesChange事件。而每个字段配置的lifecycleCallbacks怎么在onValuesChange事件执行呢?

这时需要借助FormModel管理器FormModel管理器负责接收fieldsConfig,并创建Field实例。并通过实例触发点Field内部的静态方法。

你可能觉得有点大材小用了,不过经验证明FormModel 和 FieldModel 是一种非常优雅的面向对象 + 领域驱动设计的表单管理方案。

export class FormModel {
  private fields: Record<string, FieldModel> = {}; // 存储表单字段的fieldModel实例

  constructor(params: {
    fieldsConfig: Record<string, FormFieldConfig>; // 字段配置项
    formInstance: FormInstance; // 表单实例
    lifecycleCallbacks: Record<string, IFieldLifecycleCallbacks>; // 表单特定生命周期阶段触发的回调函数集合
    initialValues?: Record<string, any>; // 表单默认值
  }) {
    const {
      fieldsConfig,
      lifecycleCallbacks,
      formInstance,
      initialValues,
    } = params;

    Object.keys(fieldsConfig).forEach(key => {
      const fieldConfig = fieldsConfig[key];
      const field = new FieldModel({
        fieldConfig,
        changeSelfValue: (value:any) => {
          formInstance.setFieldsValue({
            [fieldConfig.name]: value,
          });
        },
        lifecycleCallbacks: lifecycleCallbacks?.[fieldConfig.name],
        initialValue: initialValues,
      });
      this.fields[fieldConfig.name] = field;
    });
  }

  // 表单值更改的回调函数,遍历表单字段,挨个执行afterValueChange方法
  onFormValueChange(name: string, value: any): void {
    Object.values(this.fields).forEach(field => {
      field?.afterValueChange(name, value);
    });
  }
}
export class FieldModel<T = any> {
  private lifecycleCallbacks: IFieldLifecycleCallbacks = {}; // 字段特定生命周期阶段触发的回调函数集合
  fieldConfig: FormFieldConfig; // 字段配置项
  private changeSelfValue: (value?: T) => void; // 修改字段本身的值
  private get field(): IField {
    return {
      changeSelfValue: this.changeSelfValue,
      clearSelfValue: this.clearSelfValue,
    };
  }

  public constructor({
    fieldConfig,
    changeSelfValue,
    lifecycleCallbacks,
  }: {
    fieldConfig: FormFieldConfig;
    changeSelfValue: (value?: T) => void;
    lifecycleCallbacks?: IFieldLifecycleCallbacks;
    initialValue?: Record<string, any>;
  }) {
    this.fieldConfig = fieldConfig;
    this.changeSelfValue = changeSelfValue;
    this.registerLifeCycle(lifecycleCallbacks);
  }

  // 更新字段特定生命周期阶段触发的回调函数集合
  public registerLifeCycle = (callbacks?: IFieldLifecycleCallbacks): void => {
    this.lifecycleCallbacks = callbacks || {};
  };

  // 清空当前字段值
  private clearSelfValue = (): void => {
    this.changeSelfValue(undefined);
  };

  // 字段值更改后的回调
  afterValueChange = (key: string, value: any) => {
    if (this.lifecycleCallbacks.afterFieldValueChange) {
      this.lifecycleCallbacks.afterFieldValueChange({
        changedKey: key,
        changedValue: value,
        field: this.field,
      });
    }
  };
}

Form 组件中调用

// 初始化表单 Model
const formModelRef = useMemo(() => {
    return new FormModel({
        fieldsConfig: formFields,
        formInstance: form,
        lifecycleCallbacks: detailFormFieldsLifecycleCallbacks,
        initialValues,
    });
}, [detailFormFieldsLifecycleCallbacks, form]);

const handleValuesChange: FormProps["onValuesChange"] = useCallback((changedValues: Record<string, any>) => {
    Object.entries(changedValues).forEach(([key, value]) => {
        // 遍历所有字段,执行每个字段的afterValueChange方法
        formModelRef?.onFormValueChange(key, value);
    });
},[formModelRef]);

<Form 
    form={form} 
    layout="vertical" 
    onValuesChange={handleValuesChange} 
    initialValues={initialValues}
>
image.png `FormModel` 和 `FieldModel`这种模式的核心优势:
  1. 🧩 关注点分离,职责单一
模型职责好处
FormModel管理表单整体生命周期、字段集合、全局联动表单层只管统筹,不关心内部实现
FieldModel管理单个字段的配置、状态、生命周期回调字段自管理,内聚性强

以前:联动逻辑散落在组件里,A 字段改 B 字段,B 改 C,代码像蜘蛛网。
现在:每个字段只关心自己“被谁影响”和“影响谁”,逻辑内聚在 FieldModel 内部。

  1. 🔄 生命周期钩子,解耦联动逻辑

通过 afterFieldValueChange 这样的生命周期钩子,将联动逻辑从组件中剥离

// 组件中只需简单调用
formModelRef.onFormValueChange(key, value);

// 联动逻辑全在配置里
owner: {
  afterFieldValueChange: ({ changedKey, changedValue, field }) => {
    if (changedKey === 'department') {
      field.changeSelfValue(newValue); // 字段自己响应变化
    }
  }
}
  1. 🧠 字段自管理,高内聚

每个 FieldModel 拥有:

  • 自己的配置 (fieldConfig)
  • 自己的状态管理方法 (changeSelfValue, clearSelfValue)
  • 自己的生命周期回调

字段就像一个有“脑子”的独立个体

“我是 owner 字段,当 department 变化时,我要更新自己的选项,并清空自己的值。”

这种自管理的模式,让代码更符合人类的思维方式。

  1. 📦 封装性强,外部只暴露必要接口
// 外部只需知道这两个方法
getFieldModelByName(name: string): FieldModel
onFormValueChange(name: string, value: any): void

内部实现(字段如何响应、如何更新)完全封装在 Model 内部,外部无需关心。
这符合迪米特法则(最少知识原则),降低了系统的耦合度。

  1. 🚀 性能优化的天然基础

因为每个字段的状态独立管理,可以轻松实现:

  • 字段级渲染:只有受影响的字段重新渲染
  • 细粒度更新:避免整个表单重绘

配合 React 的 useMemomemo,能实现极致的性能优化。

  1. ♻️ 高复用性

一套 Model 可以在不同场景复用:

  • 创建表单
  • 编辑表单
  • 详情查看(只读模式)
  • 多步表单

只需要传入不同的配置和生命周期回调,就能表现出不同的行为。

  1. 📝 配置驱动,易于维护
// 字段配置集中管理
const formFields = {
  name: { type: 'INPUT', label: '名称' },
  age: { type: 'NUMBER', label: '年龄' },
  // ...
};

// 生命周期回调集中配置
const lifecycleCallbacks = {
  age: {
    afterFieldValueChange: ({ changedKey, changedValue, field }) => {
      // 年龄变化时禁用名称
      if (changedKey === 'age') {
        field.changeSelfValue(changedValue > 18 ? '成年人' : '未成年人');
      }
    }
  }
};

一目了然:哪些字段有联动,联动逻辑是什么,都在配置里写得清清楚楚。

  1. 🏗️ 扩展性强

这种模式天然支持未来扩展:

  • 添加新的生命周期(如 beforeValidateafterSubmit
  • 支持异步联动(在回调里返回 Promise)
  • 集成校验逻辑
  • 支持字段间复杂依赖链

只要在 FieldModelFormModel 中增加相应方法,就能轻松扩展。

FormModel + FieldModel 的设计,本质上是将表单从“被动接收变化的 UI 集合”升级为“主动管理状态和行为的领域模型”。
它让表单有了“生命”,每个字段都知道自己该做什么,从而让代码更清晰、更可维护、更易测试。

其他场景处理

1️⃣ Form.List 组件

Form.List用于处理动态表单项,可以在一个表单中处理多个字段的动态增删,且可以实现字段间的联动。你可以在 Form.List 中使用 dependencies 或 shouldUpdate 来实现联动。

<Form.List name="dynamicFields">
  {(fields, { add, remove }) => (
    <>
      {fields.map(({ key, name, fieldKey, ...restField }) => (
        <Form.Item
          {...restField}
          label={`Field ${name}`}
          name={[name, 'field']}
          fieldKey={[fieldKey, 'field']}
        >
          <Input />
        </Form.Item>
      ))}
      <Form.Item>
        <Button onClick={() => add()} type="dashed">
          Add Field
        </Button>
      </Form.Item>
    </>
  )}
</Form.List>

2️⃣ useEffect

在 React 组件中,结合 useState 和 useEffect 钩子可以控制表单字段的联动。通过 useEffect 监听某个字段的变化,然后更新其他字段。

const [field1, setField1] = useState('');

useEffect(() => {
  if (field1 === 'specificValue') {
    // 执行相关操作,更新其他字段
  }
}, [field1]);

return (
  <Form>
    <Form.Item name="field1">
      <Input value={field1} onChange={e => setField1(e.target.value)} />
    </Form.Item>
    <Form.Item name="field2">
      <Input />
    </Form.Item>
  </Form>
);

源码地址:github.com/nanfriend-1…