1 前言
form
几乎是 web 开发中最常用的元素之一,而作为前端接口仔和表单的关系可以说紧密而不可分割。在本文中将介绍在 React
中受控和非受控表单是如何使用的,以及现代化使用 hooks
来管理 form
状态。
2 受控和非受控表单差异
2.1 受控表单的特点和使用场景
受控表单是指表单元素的值受 React 组件的 state 或 props 控制。 特点:
表单元素的值保存在组件的 state 中,以便在需要时进行访问、验证或提交。每当用户输入发生变化时,需要手动更新 state 来反映新的值。可以通过 state 的值来进行表单元素的验证,并提供实时的错误提示。
使用场景:
- 需要对用户输入进行验证和处理的表单
- 需要实时反映用户输入的值的表单
- 需要根据表单元素的值动态地改变其他组件的状态或行为等情况时会使用到受控表单 示例代码:
javascript
import React, { useState } from 'react';
function ControlledForm() {
const [phone, setPhone] = useState('');
const handlePhoneChange = (e) => {
setName(e.target.value);
}
const handleSubmit = (e) => {
e.preventDefault();
// 处理表单提交逻辑
}
return (
<form onSubmit={handleSubmit}>
<label>
Phone:
<input type="text" value={phone} onChange={handlePhoneChange} />
</label>
<button type="submit">Submit</button>
</form>
);
}
export default ControlledForm;
2.2 非受控表单的特点和使用场景
非受控表单是指表单元素的值不受 React 组件的 state 或 props 控制,而是将表单数据交给 DOM 节点来处理,可以使用 Ref 来获取数据。 特点:
表单元素的值不会保存在组件的 state 中,而是通过 DOM 来获取。
可以通过 ref 来获取表单元素的值,而不需要手动更新 state。
不需要处理 state 的变化,可以减少代码量。
使用场景:
- 对于简单的表单,不需要对用户输入进行验证和处理。
- 需要获取表单元素的值进行一些简单的操作,如发送请求或更改 URL 等。
javascript
import React, { useRef } from 'react';
function UncontrolledForm() {
const nameInputRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
const name = nameInputRef.current.value;
}
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input type="text" ref={nameInputRef} />
</label>
<button type="submit">Submit</button>
</form>
);
}
export default UncontrolledForm;
2.3 对比受控和非受控表单的差异
特点 | 受控表单 | 非受控表单 |
---|---|---|
value 管理 | 🙆受控表单元素的值保存在组件的 state 中,方便访问和操作 | 🙅非受控组件需要依赖 ref 来获取元素值,并且会受到组件生命周期变更而影响值 |
验证和实时性 | 🙆可以实时验证和处理用户输入 | 🙅不利于实时反映用户输入的值,不方便对用户输入进行验证和处理 |
表单的整体控制 | 🙆对表单数据有更好的控制 | 🙅对表单数据的控制有限 |
数据流 | 🙆可以根据表单元素的值动态地改变其他组件的状态或行为 | 🙅需要通过 ref 来获取表单元素的值,不符合 React 的数据流思想。 |
代码复杂性 | 🙅需要更多的代码来处理表单元素的变化和验证。对于复杂的表单,可能会引入大量的 state 和事件处理函数,导致代码冗长。 | 🙆代码量较少,不需要处理 state 的变化。对于简单的表单,可以更快地实现功能。 |
dom更新性能 | 🙅 频繁的 setState 触发视图的重新渲染可能会导致性能问题。 | 🙆通过 defaultValue 来设置组件的默认值,它仅会被渲染一次,在后续的渲染时并不起作用 |
使用场景 | 基本为最佳实践 | 一般作为简易实现 |
3 使用 Hooks 管理 form 的优势
以 ant3 到 ant4 的差异为例
antd3 中form
组件设计思想:
使用HOC
(高阶组件)包裹 form
表单,HOC
组件中的 state
存储所有的控件 value
值,定义设置值和获取值的方法
存在缺陷:
由于 HOC 的设计 ,state
存于顶级组件,即便只有一个表单控件 value
值改变,所有的子组件也会因父组件 rerender
而 render
,浪费了性能
总结:
ant3
时代的 form
可以说"完美"继承了受控表单的缺点,getFieldDecorator
的 HOC
包裹表单控件的形式,并没有对 Field
自身管理状态。一个表单控件 value
值改变,便会影响整个表单查询渲染
antd4 中 form
组件设计思想:
使用 Context
包裹 form
表单,并在 useForm()
时创建一个 FormStore
实例,并通过 useRef
缓存所有的表单 value 值,定义设置值和获取值得方法。
利用 useRef
的特性,在调用 useForm
的组件中,从创建到销毁等各种生命周期,无论组件渲染多少次,FormStore
只会实例化一次,在每个 Field
中定义 forceUpdate()
强制更新组件。
kotlin
// rc-form-field
// Field.tsx
public reRender() {
if (!this.mounted) return;
this.forceUpdate();
}
.....
public onStoreChange: FieldEntity['onStoreChange'] = (prevStore, namePathList, info) => {
...
case 'remove': {
if (shouldUpdate) {
this.reRender();
return;
}
break;
}
case 'setField': {
if (namePathMatch) {
const { data } = info;
// FieldData 处理,touched/warning/error/validate
...
this.dirty = true;
this.triggerMetaEvent();
// setField 时 field 绑定 name 匹配时强制更新
this.reRender();
return;
}
// setField 携带 shouldUpdate 的控件时更新
if (
shouldUpdate &&
!namePath.length &&
requireUpdate(shouldUpdate, prevStore, store, prevValue, curValue, info)
) {
this.reRender();
return;
}
break;
case 'dependenciesUpdate': {
/**
* 当标记了的`dependencies`更新时触发. 相关联的`Field`会更新
*/
const dependencyList = dependencies.map(getNamePath);
// No need for `namePathMath` check and `shouldUpdate` check, since `valueUpdate` will be
// emitted earlier and they will work there
// dependencies 不应和 shouldUpdate 一起使用,可能会导致没必要的 rerender
if (dependencyList.some(dependency => containsNamePath(info.relatedFields, dependency))) {
this.reRender();
return;
}
break;
}
default:
if (
namePathMatch ||
((!dependencies.length || namePath.length || shouldUpdate) &&
requireUpdate(shouldUpdate, prevStore, store, prevValue, curValue, info))
) {
this.reRender();
return;
}
break;
总结:
rc-form-field
中用 useRef
缓存表单状态,使得表单状态不会直接受控件影响,而是在 setField
/shouldUpdate
/dependenciesUpdate
等逻辑触发时强制更新相依赖的控件,不会造成整个表单重新渲染的过多损耗。另外区别于 ant3
中 HOC
形式包裹的控件,rc-form-field
中提供的独立的 Field
组件概念和对应的 hooks
,提供对控件本身直接操作的可能
4 不走寻常路的 react-hook-form
不同于 rc-field-form 中使用的受控表单来做表单状态管理,
react-hook-form
使用了 React 的useRef
和useReducer
来处理表单数据的状态,而不是使用 React 的useState
来追踪表单数据的变化。具备非受控表单的优点以提高性能,并使代码更简洁。react-hook-form
的最简demo
如下
javascript
import React from "react";
import { useForm } from "react-hook-form";
function MyForm() {
const onSubmit = (data) => {
console.log(data);
};
const { register, handleSubmit, formState: { errors } } = useForm();
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("firstName", { required: true })} />
{errors.firstName && <p>First name is required.</p>}
<input {...register("lastName", { required: true })} />
{errors.lastName && <p>Last name is required.</p>}
<button type="submit">Submit</button>
</form>
);
}
为什么会说 react-hook-form
提供的是一个非受控表单,其实就需要细究一下这个 ...register
到底返回了什么
arduino
// react-hook-form createFormControl
const register: UseFormRegister<TFieldValues>
可以看到 register
返回里并没有 value
字段,那么这个表单控件的值并不受控,state
只存于控件内部,对控件的更新也只会影响自身的更新。
以非受控表单形式实现的 react-hook-form
采用订阅模式来实现不同场景:
- 表单状态订阅:可以订阅整个表单的状态,包括表单是否被提交、是否正在提交、表单数据等。通过使用
watch
函数,可以订阅表单字段的值的变化,并在变化时执行相应的回调函数。 - 表单字段订阅:可以订阅单个表单字段的状态,包括字段是否被触摸过、是否被校验过、校验错误信息等。通过使用
register
函数注册表单字段时,可以通过onChange
和onBlur
参数来订阅字段的变化和失焦事件,并在事件发生时执行相应回调函数。 - 表单校验订阅:可以订阅表单校验的状态,包括整个表单的校验结果、单个字段的校验结果等。通过使用
handleSubmit
函数提交表单时,可以获取表单的校验结果,并在校验不通过时执行相应的回调函数。 - 表单提交订阅:可以订阅表单的提交事件,包括表单提交成功和失败的情况。通过使用
handleSubmit
函数提交表单时,可以在提交成功或失败时执行相应的回调函数。
推荐阅读
招贤纳士
政采云技术团队(Zero),Base 杭州,一个富有激情和技术匠心精神的成长型团队。规模 500 人左右,在日常业务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料体系、工程平台、性能体验、可视化等领域进行技术探索和实践,推动并落地了一系列的内部技术产品,持续探索技术的新边界。此外,团队还纷纷投身社区建设,目前已经是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等众多优秀开源社区的贡献者。
如果你想改变一直被事折腾,希望开始折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊......如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的技术团队的成长过程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 zcy-tc@cai-inc.com
微信公众号
文章同步发布,政采云技术团队公众号,欢迎关注