• React 中受控表单和非受控表单的应用
  • 发布于 2个月前
  • 297 热度
    0 评论
一. 前言
form 几乎是 web 开发中最常用的元素之一,而作为前端接口仔和表单的关系可以说紧密而不可分割。在本文中将介绍在 React 中受控和非受控表单是如何使用的,以及现代化使用 hooks 来管理 form 状态。

二.受控和非受控表单差异
2.1 受控表单的特点和使用场景

受控表单是指表单元素的值受 React 组件的 state 或 props 控制。

特点:

.表单元素的值保存在组件的 state 中,以便在需要时进行访问、验证或提交。

.每当用户输入发生变化时,需要手动更新 state 来反映新的值。

.可以通过 state 的值来进行表单元素的验证,并提供实时的错误提示。


使用场景:
1.需要对用户输入进行验证和处理的表单
2.需要实时反映用户输入的值的表单
3.需要根据表单元素的值动态地改变其他组件的状态或行为等情况时会使用到受控表单 示例代码:
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 等。
import React, { useRef } from 'react';
 // 堆代码 duidaima.com
 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 来设置组件的默认值,它仅会被渲染一次,在后续的渲染时并不起作用
使用场景 基本为最佳实践 一般作为简易实现
三.使用 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() 强制更新组件。

// 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': {
  /**
  * 堆代码 duidaima.com
  * 当标记了的`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,提供对控件本身直接操作的可能

四.不走寻常路的 react-hook-form

不同于 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 采用订阅模式来实现不同场景。
用户评论