如何用Ant Design 全家桶优雅的摸鱼

5,536 阅读7分钟

前言

本来只是想在内部分享一下这个新的API使用过后的实践总结。但是我没有在掘金上找到一篇关于这个的文章,甚至github的issue上搜索关键字也只能搜索到很少的内容,所以把我自己平时使用遇到的一些坑点写出来,方便大家使用的时候减少不必要排坑时间,把更多的时间用来在掘金摸鱼

范围:本文适用于使用Ant Design全家桶的同学(包括ProComponent

useFormInstance介绍和使用

定义

获取当前上下文正在使用的 Form 实例,常见于封装子组件消费无需透传 Form 实例

type Form.useFormInstance = (): FormInstance

就是提供一个返回为Form实例的方法在子组件直接使用,无需从父组件给子组件传递form,如下:

import ProForm, { ProFormText } from '@ant-design/pro-form';
import { Form } from 'antd';

const GrandSonComp = () => {
  const form = Form.useFormInstance();
  console.log(form, 'form'); //可以获取到根节点组件定义的form实例

  return (
    <>
      <ProFormText name={'grandSonAge'} label="孙年龄" />;
    </>
  );
};

const ChildComp = () => {
  const form = Form.useFormInstance();
  console.log(form, 'form'); //可以获取到根节点组件定义的form实例

  return (
    <>
      <ProFormText name={'sonAge'} label="子年龄" />;
      <GrandSonComp />
    </>
  );
};

const FatherComp = () => {
  const [form] = Form.useForm();
  return (
    <ProForm form={form}>
      <ChildComp />
    </ProForm>
  );
};

export default FatherComp;

以上就是使用的方式,其实可以看出,不管是第一层级的子组件,还是再深层级的组件,只要是被包裹在ProForm/Form里的子组件去使用useFormInstance,都可以拿到实例,对于组件层级很深的代码很方便,免去了各种form的props传递。

注意事项

温馨提示:这个方法只能在ProForm/Form的子组件去使用,在根节点(Form同层级组件页)的页面调用useFormInstance并不会返回实例,根节点的form来自Form.useForm()。

useWatch介绍和使用

这个api我认为是Ant Design的Form这么些个版本迭代里最有用的一个新增变更。

定义

type Form.useWatch = (namePath: NamePath, formInstance: FormInstance): Value

提供了一个类似vue的监听方法(虽然,我没用过vue),在我使用过的库去寻找的话,那就是react-hooks-form了,以前写ReactNative时候用到过😳(还在饱受ReactNative折磨的选手可以了解下这个库,很强,不光是它的useWatch。)

以前的写法

转到正题,当我看到Form组件增加了这个功能后,我权衡了下,果断放弃了之前觉得很好用,可能也确实很好用,他们的同组的衍生产品ProComponent中的ProFormDependency,先来看下以下代码:

import ProForm, { ProFormDependency, ProFormDigit, ProFormSelect } from '@ant-design/pro-form';
import { Form } from 'antd';

interface IChild {
  label: number;
  value: string;
}
const childList: IChild[] = [
  {
    label: 11,
    value: 'Tom',
  },
  {
    label: 15,
    value: 'LL',
  },
  {
    label: 22,
    value: 'HMM',
  },
  {
    label: 33,
    value: 'Lucy',
  },
];
const ChildForm = () => {
  /** 这个form可以直接拿到,上文讲到过 */
  // const form = Form.useFormInstance();
  return (
    <>
      <ProFormDigit
        label="小盆友年龄"
        name={'age'}
        min={1}
        max={100}
        fieldProps={{ precision: 0 }}
      />
      <ProFormDependency name={['age']}>
        {({ age }) =>
          age ? (
            <ProFormSelect
              label="小盆友姓名"
              name={'childName'}
              params={{ age }}
              request={async () => {
                return childList.filter((it) => it.label <= (Number(age) ?? 0)) || [];
              }}
            />
          ) : null
        }
      </ProFormDependency>
    </>
  );
};
const TestComp = () => {
  const [form] = Form.useForm();
  return (
    <ProForm form={form}>
      <ChildForm />
    </ProForm>
  );
};

export default TestComp;

上面大概就是2个组件,年龄的输入框和一个小盆友姓名的选择框,中间存在一个依赖关系,输入了的年龄才显示姓名组件,如果小盆友列表里的年龄大于输入的年龄,则在下拉里面过滤掉。

解决的痛点

上面的实现方式应该是没啥毛病,但是为什么我会想放弃那个同样具有监听功能的ProFormDependency,其实是当你的页面联动逻辑变得复杂并且重复之后,写dependency的代码量会很多,看下下面的代码:

const ChildForm = () => {
  /** 这个form可以直接拿到,上文讲到过 */
  // const form = Form.useFormInstance();
  return (
    <>
      <ProFormDigit
        label="小盆友年龄"
        name={'age'}
        min={1}
        max={100}
        fieldProps={{ precision: 0 }}
      />
      <ProFormDependency name={['age']}>
        {({ age }) =>
          age ? (
            <ProFormSelect
              label="小盆友姓名"
              name={'childName'}
              params={{ age }}
              request={async () => {
                return childList.filter((it) => it.label <= (Number(age) ?? 0)) || [];
              }}
            />
          ) : null
        }
      </ProFormDependency>
      <ProFormText label="小盆友地址" name={'address'} />
      <ProFormDependency name={[['age'], ['address']]}>
        {({ age, address }) =>
          age && address ? <ProFormText label="小盆友xxx" name={'xxx'} /> : null
        }
      </ProFormDependency>
    </>
  );
};

当你的需求现在跟你说,要增加2个表单项,小盆友地址和小盆友xxx,地址没有依赖关系是个输入框,xxx必须在地址和年龄都填写了之后才会出现,那么就会有上面的代码,需要包2次组件。

你可能会想说,为什么不把小盆友姓名也包在最后的那个denpendency里,这样不就不用套2层了么?

讲道理,你能battle的过需求?因为你代码实现的便利性去跟需求撕?

你可能还会说,为什么不用useEffect监听字段变更?

比如你想这样监听:

const age = form.getFieldValue('age');
useEffect(() => {
    xxxx //你要进行的操作
 }, [age])

form.getXXX这个跟 state不一样,不会因为age的变更重新渲染你的页面,而且也监听不到你这个变更,相当于你在return上面写了个let a=1;你在下面某个方法写个a=2,你页面渲染出来的可还是1。

上面是伪代码,这个例子可能不是很好,暂时想不出来一个类似的例子

那么Form是不是有一种在return外面又可以带响应效果的方法呢?这不就来了

const ChildForm = () => {
  /** 这个form可以直接拿到,上文讲到过 */
  const form = Form.useFormInstance();
  const age = Form.useWatch('age', form);
  /** 这里的address也可以不要外面的数组 2种写法都可以 */
  const address = Form.useWatch(['address'], form);

  return (
    <>
      <ProFormDigit
        label="小盆友年龄"
        name={'age'}
        min={1}
        max={100}
        fieldProps={{ precision: 0 }}
      />
      {age ? (
        <ProFormSelect
          label="小盆友姓名"
          name={'childName'}
          params={{ age }}
          request={async () => childList.filter((it) => it.label <= (Number(age) ?? 0)) || []}
        />
      ) : null}
      <ProFormText label="小盆友地址" name={'address'} />
      {age && address ? <ProFormText label="小盆友xxx" name={'xxx'} /> : null}
    </>
  );
};

  • 先看代码量来说 没数 大概少了10行以上吧,这只是基本没什么复用依赖的情况,在复杂的联动组件页,用useWatch会比ProFormDependency组件代码量少的多。
  • useWatch返回的是一个属性的监听,这个值是实时的,跟上面form.getXXX拿到的那个完全不是一回事儿。

useWatch的踩坑记录

这里面说到的不算是坑点吧,只能说需要仔细看下文档。

只可检测单一属性值

上面的话,说明白点儿就是,你只能用useWatch返回小盆友年龄,如果你还想返回小盆友地址,麻烦再写一行。以下写法不正确:

// error
const { age,address } = useWatch(["age","address"],form)

//true
const age = useWatch(["age"],form);
const address = useWatch(["address"],form);

useWatch的第一个参数接收的是NamePath类型,是表单项的name值,而不是接受多个表单项的name值集合。

const oldName = useWatch(["name","oldName"],form);
const currentName = useWatch(["name","currentName"],form);

不存在嵌套关系的值可以传入字符串,或者数组包裹住;存在嵌套层级的值,比如name是个对象,里面存在oldName和currentName

只能监听到存在name的值

有点拗口,大概就是,你监听的属性必须存在于Form.item的name上

// isOk
const age = useWatch("age",form)

...some code

<ProFormText name={"age") />

有的同学喜欢把form当做一个全局的对象去处理,比如在form存一些自定义的属性,比如:


const extraCode = useWatch(["extraCode"],form)

const handleXXX=()=>{
   form.setFieldsValue({
       extraCode:‘xxxx'
   })
}

// ...some code

// 并拿不到
console.log(extraCode,1111)

想在form里存个extraCode用来判断。但是这个时候用useWatch拿值,并不会拿到你期望的’xxxx'而是undefined,因为watch可监听的是namePath,没有name哪来的监听呢。

  • 所以如果你硬要存个,第一种,存个useModel,存个dva的全局变量。
  • 第二种就是下面这个骚操作了。
<Form.Item noStyle name="feeSuccess">{''}</Form.Item>

就是随便在ProForm里找个地儿,写个空的Item,用noStyle去掉样式 然后给上name。。。

这是之前同事特别想这么存试出来的临时解决方式,有知道怎么优雅解决的评论区感谢大佬们留言。

总结

  • 其实还有一些项目遇到的比较复杂的问题,但是其实跟useWatch关系不大,不该甩锅给它,所以不提了就,大家自己细品吧。
  • ProFormDependency也有他的优势所在,比如可以同时监听多个属性值,比如没在name里的也可以监听到(也许,大概。)等,只能说在不同的场景有各自的优势。
  • Ant团队新增加的这两个API跟我本身业务上的契合度很高,结合自己平时遇到的问题给大家提前踩了下坑,也希望官方或者有大佬可以拓展下功能。
  • 格式上借鉴了子奕老师的这篇文章技术文章的写作技巧分享 - 掘金 (juejin.cn),格式规范方面的点对我帮助很大内容写的很详细。

提示:作为一个介绍API的文章,我只能尽可能的严谨,有不对的地方,评论区欢迎指正~

参考文档