Watch, a more elegant way to monitor antd Form field changes

南小北
4 min readDec 7, 2022

--

When using antd Form, it is a very common requirement to monitor the change of a certain field and render different UI according to different field values.

So in antd Form, how to monitor the change of a certain field?

1. form.getFieldValue

Before antd@4.20.0, assuming that we want to monitor the changes of the song field, we can easily write code like this:

const [form] = Form.useForm();
const songValue = form.getFieldValue('song');

<Form form={form}>
<Form.Item label="Song" name="song">
<Input />
</Form.Item>
{songValue?.length > 0 && <div>Song: {songValue}</div>}
</Form>;

What’s wrong with using form.getFieldValue?

The problem is that the value obtained by form.getFieldValue does not trigger UI updates. Simply put, it is not a state (the behavior of Form has changed with the upgrade of antd, and the behavior of old versions will not be discussed here).

But why sometimes, you can see the UI updated?

This is because in the actual business code, multiple interfaces may be requested (setState multiple times), and the Form may be updated by the parent to trigger re-render (in short, we all know that the number of re-renders of a React component is very high. Uncontrollable, especially when the code is poorly written 😜).

So it is not songValue that triggers the UI update, but in the new re-render, songValue is also updated.

This is a point that is very prone to bugs. Maybe the UI is normal during development, but as mentioned above, “the number of re-renders is very uncontrollable”, maybe a certain re-render is not triggered, and the UI related to songValue is also It will not be updated.

2. useState

We found a bug where the UI doesn’t update! So I naturally thought of: turn song into a state.

const [form] = Form.useForm();
const [songValue, setSongValue] = useState('');

<Form form={form}>
<Form.Item label="Song" name="song">
<Input onChange={(event) => setSongValue(event.target.value)} />
</Form.Item>
{songValue?.length > 0 && <div>Song: {songValue}</div>}
</Form>;

What’s wrong with using useState?

Put it simply, songValue here is not responsive, when song is updated using the built-in method of Form, the UI related to songValue will not be updated.

For example, when song is related to props changes, or related to interface data changes, when using form.setFieldsValue, form.resetFields, etc. to update form data, songValue will not be updated.

At this time, it is necessary to trigger setSongValue at the place where form.setFieldsValue and so on are executed.

When form.setFieldsValue is executed in a very high parent component, it is necessary to lifting state songValue up. To avoid this trouble, we prefer to handle changes in the child component’s useEffect.

Using useEffect not only triggers an extra re-render, but what if there are many fields that need to be processed in multiple places? (common situation in actual development)

We need to add a lot of repetitive code, write it back and forth, and forget where it is not added, where it needs to be added, and where it does not need to be added. In the end, the update logic will become a mess.

3. Form.useWatch

At the beginning, it was emphasized before antd@4.20.0, because from antd@4.20.0, antd Form added a new API Form.useWatch to handle this situation.

At this point, songValue can respond to the updates of form.setFieldsValue, form.resetFields, etc.

const [form] = Form.useForm();
const songValue = Form.useWatch('song', form);

<Form form={form}>
<Form.Item label="Song" name="song">
<Input />
</Form.Item>
{songValue?.length > 0 && <div>Song: {songValue}</div>}
</Form>;

What’s wrong with using Form.useWatch? (Why is there still a problem!)

In fact, it is not a problem, the main thing is that the performance is not good. Because Form.useWatch actually turns songValue into a state, and then handles the form linkage internally.

But the problem with state is that it will trigger the re-render of the entire component and perform unnecessary diffs. If the component is large and listens to Input for real-time input, this kind of performance consumption is terrible, and each key is one time. Full diff.

And this kind of re-render is actually meaningless, because we “precisely” know that we need to monitor the changes of the song field, and update the "partial" UI according to the value of song instead of updating the overall UI.

So is there a more elegant “partial update” solution?

4. Watch, from Ant Plus 5

Ant Plus 5 (antx) provides a Watch component, which is dedicated to listening to changes in form fields and updating local UI requirements.

Using the antx component can simplify the antd Form code, then the code to listen to song will be as follows:

import { Form, Watch, Input } from 'antx';

const [form] = Form.useForm();
<Form form={form}>
<Input label="Song" name="song" />
<Watch name="song">
{(songValue) => {
// Only update in this place,
// not trigger the whole component re-render every time
return songValue?.length > 0 && <div>Song: {songValue}</div>;
}}
</Watch>
</Form>;

By using Watch, you can avoid the performance problem of Form.useWatch non-stop full re-render, and at the same time, you don’t need to process the update logic in useEffect.

Using Watch, only the UI returned in the render props will be updated, and the entire component will not be linked to re-render continuously.

Online example → https://codesandbox.io/s/antx-v4hqw

5. Introducing Watch props

Watch can also use list prop to watch multiple fields.

name and list are mutually exclusive, because name(NamePath) of antd also supports array form, so list is used to distinguish different meanings of arrays.

children & onlyValid are mutually exclusive with onChange.

Use onlyValid to get only "valid values" that are not undefined in the children function. And setState can be executed in onChange.

Welcome to the Watch component of antx.

For more information about Ant Plus 5, please check → https://github.com/nanxiaobei/ant-plus

--

--