浅谈表单受控性及结合Hooks应用

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 值改变,所有的子组件也会因父组件 rerenderrender,浪费了性能

总结:

ant3 时代的 form 可以说"完美"继承了受控表单的缺点,getFieldDecoratorHOC 包裹表单控件的形式,并没有对 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 等逻辑触发时强制更新相依赖的控件,不会造成整个表单重新渲染的过多损耗。另外区别于 ant3HOC 形式包裹的控件,rc-form-field 中提供的独立的 Field 组件概念和对应的 hooks,提供对控件本身直接操作的可能

4 不走寻常路的 react-hook-form

不同于 rc-field-form 中使用的受控表单来做表单状态管理,react-hook-form 使用了 React 的 useRefuseReducer 来处理表单数据的状态,而不是使用 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 采用订阅模式来实现不同场景:

  1. 表单状态订阅:可以订阅整个表单的状态,包括表单是否被提交、是否正在提交、表单数据等。通过使用 watch 函数,可以订阅表单字段的值的变化,并在变化时执行相应的回调函数。
  2. 表单字段订阅:可以订阅单个表单字段的状态,包括字段是否被触摸过、是否被校验过、校验错误信息等。通过使用 register 函数注册表单字段时,可以通过 onChangeonBlur 参数来订阅字段的变化和失焦事件,并在事件发生时执行相应回调函数。
  3. 表单校验订阅:可以订阅表单校验的状态,包括整个表单的校验结果、单个字段的校验结果等。通过使用 handleSubmit 函数提交表单时,可以获取表单的校验结果,并在校验不通过时执行相应的回调函数。
  4. 表单提交订阅:可以订阅表单的提交事件,包括表单提交成功和失败的情况。通过使用 handleSubmit 函数提交表单时,可以在提交成功或失败时执行相应的回调函数。

推荐阅读

Mybatis一级缓存问题

MySQL死锁浅析

探索Taro:跨平台开发的实践与原理

spring如何使用三级缓存解决循环依赖

化繁为简:Flutter组件依赖可视化

招贤纳士

政采云技术团队(Zero),Base 杭州,一个富有激情和技术匠心精神的成长型团队。规模 500 人左右,在日常业务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料体系、工程平台、性能体验、可视化等领域进行技术探索和实践,推动并落地了一系列的内部技术产品,持续探索技术的新边界。此外,团队还纷纷投身社区建设,目前已经是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等众多优秀开源社区的贡献者。

如果你想改变一直被事折腾,希望开始折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊......如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的技术团队的成长过程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 zcy-tc@cai-inc.com

微信公众号

文章同步发布,政采云技术团队公众号,欢迎关注

相关推荐
浮华似水6 分钟前
Javascirpt时区——脱坑指南
前端
王二端茶倒水8 分钟前
大龄程序员兼职跑外卖第五周之亲身感悟
前端·后端·程序员
_oP_i13 分钟前
Web 与 Unity 之间的交互
前端·unity·交互
钢铁小狗侠15 分钟前
前端(1)——快速入门HTML
前端·html
凹凸曼打不赢小怪兽41 分钟前
react 受控组件和非受控组件
前端·javascript·react.js
狂奔solar1 小时前
分享个好玩的,在k8s上部署web版macos
前端·macos·kubernetes
qiyi.sky1 小时前
JavaWeb——Web入门(8/9)- Tomcat:基本使用(下载与安装、目录结构介绍、启动与关闭、可能出现的问题及解决方案、总结)
java·前端·笔记·学习·tomcat
清云随笔1 小时前
axios 实现 无感刷新方案
前端
鑫宝Code1 小时前
【React】状态管理之Redux
前端·react.js·前端框架
忠实米线1 小时前
使用pdf-lib.js实现pdf添加自定义水印功能
前端·javascript·pdf