1 前言
form
几乎是 web 开发中最常用的元素之一,而作为前端接口仔和表单的关系可以说紧密而不可分割。在本文中将介绍在 React
中受控和非受控表单是如何使用的,以及现代化使用 hooks
来管理 form
状态。
受控表单是指表单元素的值受 React 组件的 state 或 props 控制。特点:
表单元素的值保存在组件的 state 中,以便在需要时进行访问、验证或提交。每当用户输入发生变化时,需要手动更新 state 来反映新的值。可以通过 state 的值来进行表单元素的验证,并提供实时的错误提示。
使用场景:
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;
非受控表单是指表单元素的值不受 React 组件的 state 或 props 控制,而是将表单数据交给 DOM 节点来处理,可以使用 Ref 来获取数据。特点:
表单元素的值不会保存在组件的 state 中,而是通过 DOM 来获取。
可以通过 ref 来获取表单元素的值,而不需要手动更新 state。
不需要处理 state 的变化,可以减少代码量。
使用场景:
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;
特点 | 受控表单 | 非受控表单 |
---|---|---|
value 管理 | 🙆受控表单元素的值保存在组件的 state 中,方便访问和操作 | 🙅非受控组件需要依赖 ref 来获取元素值,并且会受到组件生命周期变更而影响值 |
验证和实时性 | 🙆可以实时验证和处理用户输入 | 🙅不利于实时反映用户输入的值,不方便对用户输入进行验证和处理 |
表单的整体控制 | 🙆对表单数据有更好的控制 | 🙅对表单数据的控制有限 |
数据流 | 🙆可以根据表单元素的值动态地改变其他组件的状态或行为 | 🙅需要通过 ref 来获取表单元素的值,不符合 React 的数据流思想。 |
代码复杂性 | 🙅需要更多的代码来处理表单元素的变化和验证。对于复杂的表单,可能会引入大量的 state 和事件处理函数,导致代码冗长。 | 🙆代码量较少,不需要处理 state 的变化。对于简单的表单,可以更快地实现功能。 |
dom更新性能 | 🙅 频繁的 setState 触发视图的重新渲染可能会导致性能问题。 | 🙆通过 defaultValue 来设置组件的默认值,它仅会被渲染一次,在后续的渲染时并不起作用 |
使用场景 | 基本为最佳实践 | 一般作为简易实现 |
以 ant3 到 ant4 的差异为例
form
组件设计思想:使用HOC
(高阶组件)包裹 form
表单,HOC
组件中的 state
存储所有的控件 value
值,定义设置值和获取值的方法
存在缺陷:
由于 HOC 的设计 ,state
存于顶级组件,即便只有一个表单控件 value
值改变,所有的子组件也会因父组件 rerender
而 render
,浪费了性能
总结:
ant3
时代的 form
可以说“完美”继承了受控表单的缺点,getFieldDecorator
的 HOC
包裹表单控件的形式,并没有对 Field
自身管理状态。一个表单控件 value
值改变,便会影响整个表单查询渲染
form
组件设计思想:使用 Context
包裹 form
表单,并在 useForm()
时创建一个 FormStore
实例,并通过 useRef
缓存所有的表单 value 值,定义设置值和获取值得方法。
利用 useRef
的特性,在调用 useForm
的组件中,从创建到销毁等各种生命周期,无论组件渲染多少次,FormStore
只会实例化一次,在每个 Field
中定义 forceUpdate()
强制更新组件。
// 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
,提供对控件本身直接操作的可能
不同于 rc-field-form 中使用的受控表单来做表单状态管理,
react-hook-form
使用了 React 的useRef
和useReducer
来处理表单数据的状态,而不是使用 React 的useState
来追踪表单数据的变化。具备非受控表单的优点以提高性能,并使代码更简洁。react-hook-form
的最简demo
如下
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
到底返回了什么
// react-hook-form createFormControl
const register: UseFormRegister<TFieldValues>
可以看到 register
返回里并没有 value
字段,那么这个表单控件的值并不受控,state
只存于控件内部,对控件的更新也只会影响自身的更新。
以非受控表单形式实现的 react-hook-form
采用订阅模式来实现不同场景