前言
本来只是想在内部分享一下这个新的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的文章,我只能尽可能的严谨,有不对的地方,评论区欢迎指正~