发现掘金上没有对Antd的Form组件内部调用API的细节有详细解释的文章,今天来说一下吧。
今天在编写表单组件的时候调用了Antd组件库,采用Form表单构建组件。
import { FC, useEffect } from 'react';
import { Form, Input, Checkbox, Select, Button, Space } from 'antd'
import { PlusOutlined } from '@ant-design/icons';
import { QuestionRadioPropsType, QuestionRadioDefaultProps } from './interface'
const PropComponent: FC<QuestionRadioPropsType> = (props: QuestionRadioPropsType) => {
const { title, isVertical, options = [], value, onChange, disabled } = { ...QuestionRadioDefaultProps, ...props };
const [form] = Form.useForm();
//监听options字段变化,确保实时获取最新选项
const watchOptions = Form.useWatch('options', form) || [];
useEffect(() => {
form.setFieldsValue({ title, isVertical, options, value });
}, [title, isVertical, options, value]);
//接收Antd传过来的参数:changedValues(变化的字段), allValues(所有字段)
function handleValueChange(changedValues: any, allValues: any) {
if (onChange) {
//错误:这里拿到的可能是未更新的旧数据
onChange(form.getFieldsValue());
}
}
return (
<Form
layout="vertical"
initialValues={{ title, isVertical, options, value }}
onValuesChange={handleValueChange}
disabled={disabled}
form={form}
>
<Form.Item
label="标题"
name="title"
rules={[
{
required: true,
message: '请输入标题'
}
]}>
<Input />
</Form.Item>
<Form.Item label="选项">
<Form.List name="options">
{
(fields, { add, remove }) => (
<>
{/* 遍历所有的选项(可删除) */}
{fields.map(({ key, name, ...restField }, index) => {
return (
<Space key={key}>
<Form.Item
{...restField}
name={[name, 'text']}
rules={[
{
required: true,
message: '请输入选项内容'
}
]}>
<Input placeholder="请输入选项内容" />
</Form.Item>
</Space>
)
})}
{/* 添加选项 */}
<Form.Item>
<Button
type="link"
onClick={() => add({ text: '', value: `${Date.now()}` })}
icon={<PlusOutlined />}
block
>添加选项</Button>
</Form.Item>
</>
)
}
</Form.List>
</Form.Item>
<Form.Item label="默认选中" name="value">
<Select
value={value}
options={watchOptions.map((opt: any, index: number) => ({
value: opt?.value || index,
label: opt?.text || ''
}))} />
</Form.Item>
<Form.Item name="isVertical" valuePropName="checked">
<Checkbox>垂直显示</Checkbox>
</Form.Item>
</Form>
)
}
export default PropComponent;
这样直接运行,在右侧点击添加选项,界面没有增加新的表单。
这是因为Antd组件库的Form内置的APIonValuesChange和getFieldsValue的调用时机导致的。
Antd Form 的值是怎么变化的?
Antd的 Form 底层是基于 rc-field-form 这个库,它有一套自己的“字段 store”(不直接挂在 React state 上)。
- 用户输入 → 触发字段组件的
onChange; - 字段组件调用
form.setFieldsValue({ [name]: value })(内部); rc-field-form更新内部字段store;- 根据需要触发:
onFieldsChangeonValuesChange- 然后触发组件重渲染,让
Form.Item拿到新 value。
注意:
- onValuesChange语义:监听表单值的变化,触发回调函数,并不保证store中的值及时更新。
- getFieldsValue语义:从当前store中读取字段值。
整体流程如下:
用户输入 --> 字段组件onChange --> form.setFieldsValue --> rc-field-form内部处理 --> 触发onValuesChange --> form.getFieldsValue读取store
rc-field-form内部处理 --> 更新字段store写入新值 --> 触发组件重渲染
onValuesChange 的调用时机早于“真正写入 store 完成”
rc-field-form 在内部处理字段更新时,会先触发 onValuesChange,再进行 store 的“提交”或其它副作用。
所以你在 onValuesChange 里立即 getFieldsValue(),读到的是上一轮的快照。
如何在onValuesChange中拿到新值?
使用 onValuesChange 的第二个参数 allValues
<Form
form={form}
onValuesChange={
(changedValues, allValues) =>
{
// 这里 allValues 就是“最新的所有字段值”
console.log(allValues);
}}
>
{
/* ... */
}
</Form>
这个 allValues 是 rc-field-form 根据即将写入的值计算出来的“最新快照”,不需要再调用 getFieldsValue()。
推荐做法:在 onValuesChange 里直接用 allValues,不要再查 form.getFieldsValue()。
如果一定要用 getFieldsValue,就等“下一帧”
在事件处理器里,React 会批处理 state 更新;Form 的 store 更新也类似“排队执行”。
所以你可以:
<Form
form={form}
onValuesChange={
(changedValues) =>
{
// 1. 这一步还是旧值
console.log('old', form.getFieldsValue());
// 2. 等当前事件处理结束、Form store 更新完成再读
setTimeout(() =>{
console.log('new', form.getFieldsValue
());
}, 0);
}}
>
{
/* ... */
}
</Form>
原理:
• setTimeout(..., 0) 会把回调放到 事件循环的下一个时机;
• 此时:
- React 的批处理已经结束;
Form的内部更新也已经提交;getFieldsValue()就能读到最新值。
改动后的项目:
import { FC, useEffect } from 'react';
import { Form, Input, Checkbox, Select, Button, Space } from 'antd'
import { PlusOutlined } from '@ant-design/icons';
import { QuestionRadioPropsType, QuestionRadioDefaultProps } from './interface'
const PropComponent: FC<QuestionRadioPropsType> = (props: QuestionRadioPropsType) => {
const { title, isVertical, options = [], value, onChange, disabled } = { ...QuestionRadioDefaultProps, ...props };
const [form] = Form.useForm();
//监听options字段变化,确保实时获取最新选项
const watchOptions = Form.useWatch('options', form) || [];
useEffect(() => {
form.setFieldsValue({ title, isVertical, options, value });
}, [title, isVertical, options, value]);
//接收Antd传过来的参数:changedValues(变化的字段), allValues(所有字段)
function handleValueChange(changedValues: any, allValues: any) {
if (onChange) {
//错误:这里拿到的可能是未更新的旧数据
//onChange(form.getFieldsValue());
/**
* handleValueChange 处理函数和useEffect相互配合出现的问题:
* 1.触发添加:你点击 add,Antd 的 Form.List 在内部状态中增加了一个新选项。
* 2.触发变化:表单内部状态改变,触发了 <Form onValuesChange={handleValueChange}>。
* 3.获取脏数据(罪魁祸首):在你的 handleValueChange 中,你使用了 form.getFieldsValue()。在 onValuesChange 触发的瞬间,由于 React 的异步更新机制,form.getFieldsValue() 有时拿到的还是添加之前的旧数据。
* 4.状态提升:你把这份旧数据通过 onChange 传给了父组件(Redux)。
* 5.重置表单:父组件接收到旧数据后,将旧的 options 作为 props 传回给当前组件。触发了 useEffect。
* 6.瞬间抹杀:useEffect 执行 form.setFieldsValue({ ... }),用旧的 options 强行覆盖了表单,导致你刚刚添加的* 新选项瞬间消失,看起来就像没加成功一样。
*
* onValuesChange语义:监听表单值的变化,触发回调函数,并不保证store中的值及时更新。
* getFieldsValue语义:从当前store中读取字段值。
*/
// 确保在 onChange 中获取到最新的所有值
onChange(allValues);
}
}
return (
<Form
layout="vertical"
initialValues={{ title, isVertical, options, value }}
onValuesChange={handleValueChange}
disabled={disabled}
form={form}
>
<Form.Item
label="标题"
name="title"
rules={[
{
required: true,
message: '请输入标题'
}
]}>
<Input />
</Form.Item>
<Form.Item label="选项">
<Form.List name="options">
{
(fields, { add, remove }) => (
<>
{/* 遍历所有的选项(可删除) */}
{fields.map(({ key, name, ...restField }, index) => {
return (
<Space key={key}>
<Form.Item
{...restField}
name={[name, 'text']}
rules={[
{
required: true,
message: '请输入选项内容'
}
]}>
<Input placeholder="请输入选项内容" />
</Form.Item>
</Space>
)
})}
{/* 添加选项 */}
<Form.Item>
<Button
type="link"
onClick={() => add({ text: '', value: `${Date.now()}` })}
icon={<PlusOutlined />}
block
>添加选项</Button>
</Form.Item>
</>
)
}
</Form.List>
</Form.Item>
<Form.Item label="默认选中" name="value">
<Select
value={value}
options={watchOptions.map((opt: any, index: number) => ({
value: opt?.value || index,
label: opt?.text || ''
}))} />
</Form.Item>
<Form.Item name="isVertical" valuePropName="checked">
<Checkbox>垂直显示</Checkbox>
</Form.Item>
</Form>
)
}
export default PropComponent;