Watch,一种更优雅的监听 antd Form 字段变化的方式

7,086 阅读4分钟

使用 antd Form 时,监听某个字段变化,并根据不同值渲染不同的 UI,是个非常常见的需求。

那么在 antd Form 中,如何监听某个字段的变化呢?

1. form.getFieldValue

antd@4.20.0 之前,假设要监听 song 字段的变化,我们很容易写出这样的代码:

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

<Form form={form}>
  <Form.Item label="歌曲" name="song">
    <Input />
  </Form.Item>

  {songValue?.length > 0 && <div>歌曲:{songValue}</div>}
</Form>;

使用 form.getFieldValue 有什么问题呢?

问题就是 form.getFieldValue 取到的值,并不会触发 UI 更新,简单来说,不是一个 state(随着 antd 升级其 Form 行为有过变化,此处不讨论旧版本行为)。

那为什么有时候,又确实看到 UI 更新了呢?

这是因为实际业务代码中,可能会请求多个接口(多次 setState),也可能 Form 会被父级更新触发 re-render(总之,我们都知道一个 React 组件的 re-render 次数是非常不可控的,尤其是代码写的很烂时 😜)。

所以并不是 songValue 触发了 UI 更新,而是在新的 re-render 中,songValue 连带着被更新了。

这里就是非常容易产生 bug 的一个点,可能开发时 UI 是正常的,但正如上面所说,"re-render 次数非常不可控",可能某次 re-render 未被触发,songValue 相关 UI 也就不更新了。

2. useState

我们发现了一个 bug,UI 竟然不更新!于是自然而然的想到:把 song 变成一个 state。

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

<Form form={form}>
  <Form.Item label="歌曲" name="song">
    <Input onChange={(event) => setSongValue(event.target.value)} />
  </Form.Item>

  {songValue?.length > 0 && <div>歌曲:{songValue}</div>}
</Form>;

使用 useState 有什么问题呢?

简单来说,此处 songValue 并不是响应式的,当用 Form 内置方法更新 song 时,songValue 相关 UI 并不会更新。

例如当 song 与 props 变化有关,或与接口数据变化有关,使用 form.setFieldsValueform.resetFields 等更新表单数据时,songValue 并不会更新。

此时就需要在执行 form.setFieldsValue 等的地方,相应的触发 setSongValue

form.setFieldsValue 在很高的父组件中执行时,又需要将 songValue 状态提升,为避免这种麻烦,我们更倾向于在子组件 useEffect 中处理变化。

使用 useEffect 不仅触发了多余的一次 re-render,而且,假如有很多字段,需要在多处处理呢?(实际开发中的常见情况)

我们需要添加大量的重复性代码,写来写去,又忘了哪里没加、哪里需要加、哪里不需要加,最终,更新逻辑会变得一团混乱。

3. Form.useWatch

一开始强调在 antd@4.20.0 之前,是因为从 antd@4.20.0 开始,antd Form 添加了一个新的 API Form.useWatch,用于处理此种情况。

此时,songValue 就可以响应 form.setFieldsValueform.resetFields 等的更新了。

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

<Form form={form}>
  <Form.Item label="歌曲" name="song">
    <Input />
  </Form.Item>

  {songValue?.length > 0 && <div>歌曲:{songValue}</div>}
</Form>;

使用 Form.useWatch 有什么问题呢?(怎么还有问题!)

其实不是问题,主要是性能不好。因为 Form.useWatch 其实就是把 songValue 变为了一个 state,然后内部处理了表单联动。

但 state 的问题就是,它会触发整个组件的 re-render,进行不必要的 diff,如果组件很大而且是监听 Input 实时输入,这种性能消耗是很恐怖的,每次按键都是一次全量 diff。

而这种 re-render 其实毫无意义,因为我们 "精准" 的知道,就是要监听 song 字段的变化,根据 song 的值来更新 "局部" 的 UI,而不是更新整体 UI。

那么有没有更优雅的 "局部更新" 的方案呢?

4. Watch 组件,来自 Ant Plus 5

Ant Plus 5(antx)中提供了一个 Watch 组件,专用于监听表单字段变化,并更新局部 UI 的需求。

使用 antx 组件,可以简化 antd Form 代码,那么监听 song 的代码将如下:

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

const [form] = Form.useForm();

<Form form={form}>
  <Input label="歌曲" name="song" />

  <Watch name="song">
    {(songValue) => {
      // 仅此处 UI 更新,不会每次输入都触发整个组件 re-render
      return songValue?.length > 0 && <div>歌曲:{songValue}</div>;
    }}
  </Watch>
</Form>;

使用 Watch,就可以避免 Form.useWatch 不停全量 re-render 的性能问题,同时,也不需要在 useEffect 中处理更新逻辑。

使用 Watch,就只有 render props 中返回的 UI 会更新,不会联动整个组件不停的 re-render。

在线示例 → codesandbox.io/s/antx-v4hq…

5. Watch props 介绍

Watch 还可以使用 list 属性监听多个字段。

namelist 互斥,因为 antdname(NamePath) 也支持数组形式,故用 list 来区分数组的不同含义。

children & onlyValidonChange 互斥。

使用 onlyValid 可在 children 函数中只拿到非 undefined 的 "有效值"。而 onChange 中可执行 setState

Props说明类型默认值
name需监听的字段NamePath-
list需监听的字段列表 (与 name 互斥)NamePath[]-
childrenRender props 形式。获取被监听的值(或列表),返回 UI(value: any) => ReactNode-
onlyValid被监听的值非 undefined 时,才触发 children 渲染booleanfalse
onChange获取被监听的值(或列表),处理副作用 (与 children 互斥) (value: any) => void-

欢迎尝试 antxWatch 组件。

更多关于 Ant Plus 5 的信息,请查看 → github.com/nanxiaobei/…