Antd组件库Form的onValuesChange和getFieldsValue的调用时机分析

9 阅读1分钟

发现掘金上没有对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内置的APIonValuesChangegetFieldsValue的调用时机导致的。
Antd Form 的值是怎么变化的?
  Antd的 Form 底层是基于 rc-field-form 这个库,它有一套自己的“字段 store”(不直接挂在 React state 上)。

  1. 用户输入 → 触发字段组件的 onChange
  2. 字段组件调用 form.setFieldsValue({ [name]: value })(内部);
  3. rc-field-form 更新内部字段 store
  4. 根据需要触发:
  • onFieldsChange
  • onValuesChange
  • 然后触发组件重渲染,让 Form.Item 拿到新 value。

注意:

  1. onValuesChange语义:监听表单值的变化,触发回调函数,并不保证store中的值及时更新。
  2. 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>

这个 allValuesrc-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;