当表单里的字段开始互相“使眼色”,你的代码还能撑住吗?
这位同学,你还在为表单联动而疯狂堆useEffect吗?
今天,我们就来盘点几种搞定复杂联动的方案,总有一款适合你!
📋数据“样板间”:一个会搞事情的表单
先来看一个经典场景 👀
任务信息收集表单,字段如下:
- 任务名称 (
taskName) —— 普通输入框 - 优先级 (
priority) —— 下拉单选 - 所属部门 (
department) —— 下拉单选 - 负责人 (
owner) —— 下拉单选(随部门 department 变化) - 节点 (
nodes) —— 下拉多选(随负责人 owner 变化)
联动规则:
- 切换部门 → 负责人选项更新
- 切换负责人 → 节点选项更新
这里只描述了选项间的联动,还会有当前值、显隐、禁用态等的联动设置。
听起来不难?但当你需要面对几十个甚至上百个字段,互相牵一发而动全身时,代码就会变成一锅粥。
表单配置
- 表单所有字段配置项
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"]
}
};
- 表单初始值
export const initialValues = {
taskName: "",
priority: "P0",
department: undefined,
owner: undefined,
nodes: []
};
- 模拟不同的选项值
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" }
]
};
- 表单控件匹配组件
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>
);
};
👎 缺点
- 逻辑分散,不要适合多对多;难以复用,换个表单就得重写一遍。
方案二:📋 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>
);
};
👎 缺点
- 异步处理需要额外状态(如 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>
);
};
👎 缺点
- 需要自己管理状态和依赖。
- 会创建多个变量。
方案四:🚀 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;
例如负责人可选项受部门的影响,那owner的lifecycleCallbacks就可以增加一个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}
>
- 🧩 关注点分离,职责单一
| 模型 | 职责 | 好处 |
|---|---|---|
FormModel | 管理表单整体生命周期、字段集合、全局联动 | 表单层只管统筹,不关心内部实现 |
FieldModel | 管理单个字段的配置、状态、生命周期回调 | 字段自管理,内聚性强 |
以前:联动逻辑散落在组件里,A 字段改 B 字段,B 改 C,代码像蜘蛛网。
现在:每个字段只关心自己“被谁影响”和“影响谁”,逻辑内聚在 FieldModel 内部。
- 🔄 生命周期钩子,解耦联动逻辑
通过 afterFieldValueChange 这样的生命周期钩子,将联动逻辑从组件中剥离:
// 组件中只需简单调用
formModelRef.onFormValueChange(key, value);
// 联动逻辑全在配置里
owner: {
afterFieldValueChange: ({ changedKey, changedValue, field }) => {
if (changedKey === 'department') {
field.changeSelfValue(newValue); // 字段自己响应变化
}
}
}
- 🧠 字段自管理,高内聚
每个 FieldModel 拥有:
- 自己的配置 (
fieldConfig) - 自己的状态管理方法 (
changeSelfValue,clearSelfValue) - 自己的生命周期回调
字段就像一个有“脑子”的独立个体:
“我是 owner 字段,当 department 变化时,我要更新自己的选项,并清空自己的值。”
这种自管理的模式,让代码更符合人类的思维方式。
- 📦 封装性强,外部只暴露必要接口
// 外部只需知道这两个方法
getFieldModelByName(name: string): FieldModel
onFormValueChange(name: string, value: any): void
内部实现(字段如何响应、如何更新)完全封装在 Model 内部,外部无需关心。
这符合迪米特法则(最少知识原则),降低了系统的耦合度。
- 🚀 性能优化的天然基础
因为每个字段的状态独立管理,可以轻松实现:
- 字段级渲染:只有受影响的字段重新渲染
- 细粒度更新:避免整个表单重绘
配合 React 的 useMemo、memo,能实现极致的性能优化。
- ♻️ 高复用性
一套 Model 可以在不同场景复用:
- 创建表单
- 编辑表单
- 详情查看(只读模式)
- 多步表单
只需要传入不同的配置和生命周期回调,就能表现出不同的行为。
- 📝 配置驱动,易于维护
// 字段配置集中管理
const formFields = {
name: { type: 'INPUT', label: '名称' },
age: { type: 'NUMBER', label: '年龄' },
// ...
};
// 生命周期回调集中配置
const lifecycleCallbacks = {
age: {
afterFieldValueChange: ({ changedKey, changedValue, field }) => {
// 年龄变化时禁用名称
if (changedKey === 'age') {
field.changeSelfValue(changedValue > 18 ? '成年人' : '未成年人');
}
}
}
};
一目了然:哪些字段有联动,联动逻辑是什么,都在配置里写得清清楚楚。
- 🏗️ 扩展性强
这种模式天然支持未来扩展:
- 添加新的生命周期(如
beforeValidate、afterSubmit) - 支持异步联动(在回调里返回 Promise)
- 集成校验逻辑
- 支持字段间复杂依赖链
只要在 FieldModel 和 FormModel 中增加相应方法,就能轻松扩展。
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>
);