简洁之道 - React Hook Form

React Hook Form 是一个基于 React hooks 的表单库,它通过提供一系列的钩子(Hook)来简化表单状态管理和验证。与传统的表单处理方式相比,React Hook Form 不仅减少了代码量,还提高了代码的可读性和可维护性。

简洁性分析

React Hook Form 实现代码简洁性的策略

  1. 减少样板代码

在传统的表单处理中,开发者往往需要编写大量的样板代码来处理表单状态、事件处理和验证逻辑。React Hook Form 通过提供 useForm 钩子,将这些繁琐的步骤抽象化,使得开发者可以专注于业务逻辑的实现。

  1. 利用 Hook API

React Hook Form 的核心是 useForm 钩子,它返回一个配置好的表单对象,包括注册表单字段、处理表单提交和获取表单状态等方法。这些方法的使用大大简化了表单逻辑的编写。

  1. 内置验证功能

React Hook Form 提供了强大的内置验证功能,支持同步和异步验证。开发者可以通过简单的配置实现复杂的验证逻辑,无需编写额外的验证代码。

  1. 避免不必要的渲染

React Hook Form 通过智能的依赖跟踪和渲染优化,避免了不必要的组件重新渲染,从而提高了应用的性能和用户体验。

下面我们来看一个例子,以收集邮箱、密码信息为例做一下对比。

不使用React Hook Form

复制代码
import React, { useState } from 'react';

const TraditionalForm = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [errors, setErrors] = useState({});

  const validate = (email, password) => {
    let validationErrors = {};
    if (!email) {
      validationErrors.email = "Email is required";
    }
    if (!password) {
      validationErrors.password = "Password is required";
    }
    return validationErrors;
  };

  const handleSubmit = (event) => {
    event.preventDefault();
    const validationErrors = validate(email, password);
    if (Object.keys(validationErrors).length === 0) {
      console.log({ email, password });
    }
    setErrors(validationErrors);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      {errors.email && <p>{errors.email}</p>}
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      {errors.password && <p>{errors.password}</p>}
      <button type="submit">Submit</button>
    </form>
  );
};

使用React Hook Form

复制代码
import React from 'react';
import { useForm } from 'react-hook-form';

const MyForm = () => {
  const { register, handleSubmit, formState: { errors } } = useForm();

  const onSubmit = data => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        name="email"
        {...register({
          required: "Email is required",
          pattern: {
            value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i,
            message: "Invalid email address"
          }
        })}
      />
      {errors.email && <p>{errors.email.message}</p>}
      <input
        name="password"
        type="password"
        {...register({ required: "Password is required" })}
      />
      {errors.password && <p>{errors.password.message}</p>}
      <button type="submit">Submit</button>
    </form>
  );
};

从上面两个例子对比可知,在传统表单处理中,我们通常需要手动维护多个表单状态(如 useState 钩子),为每个字段编写 onChange 事件处理器,并在提交时手动验证和获取表单数据。React Hook Form 通过 useForm 钩子简化了这些步骤。

而在表单字段的注册和验证方面则是通过 register 直接处理,省去了手动处理事件、表单状态等繁琐步骤。像 handleSubmit 自动处理了表单提交,并且可以通过内置的表单状态 errors 轻松获取验证结果。

同时还内置了同步和异步验证功能。通过在 register 中传入验证规则(如 { required: true }),开发者可以轻松实现验证逻辑。在传统的表单处理中,验证通常需要手动编写验证函数,并在表单提交时检查每个字段的合法性。

而在渲染方面,自己写的时候可能每次输入时都重新渲染整个表单,而使用React Hook Form之后只会在验证出错时重新渲染错误提示部分。

React Hook Form的register方法

从前面的一些例子,我们已经看到 register 的方法是用于注册表单字段的。那它内部是如何实现的?

register 方法是 react-hook-form 库的核心功能之一,它用于注册表单字段并设置相关的验证规则。以下是 register 方法实现逻辑的详细解读:

1. 初始化字段存储结构

复制代码
let _fields: FieldRefs = {};

_fields 对象用于存储所有注册字段的引用和配置信息。

2. 设置字段的默认值

复制代码
let _defaultValues = ...;

_defaultValues 存储字段的初始值,可以来自 defaultValues 属性或 values 属性。

3. 注册字段

复制代码
const register: UseFormRegister<TFieldValues> = (name, options = {}) => {
  ...
};

register 方法接受字段名 name 和可选的配置对象 options

3.1 设置字段的引用
复制代码
set(_fields, name, {
  ...(field || {}),
  _f: {
    ...(field && field._f ? field._f : { ref: { name } }),
    name,
    mount: true,
    ...options,
  },
});

_fields 对象中设置字段的引用和配置。如果字段已经存在,则合并现有的配置。

3.2 添加字段名到 _names.mount 集合
复制代码
_names.mount.add(name);

_names.mount 集合用于跟踪已挂载的字段。

3.3 更新禁用字段的状态
复制代码
if (field) {
  _updateDisabledField({
    field,
    disabled: isBoolean(options.disabled) ? options.disabled : props.disabled,
    name,
    value: options.value,
  });
} else {
  updateValidAndValue(name, true, options.value);
}

如果字段已存在,则更新禁用状态。否则,更新字段的有效值。

4. 返回字段的引用对象

复制代码
return {
  ...(disabledIsDefined ? { disabled: options.disabled || props.disabled } : {}),
  ...(_options.progressive ? {
    required: !!options.required,
    min: getRuleValue(options.min),
    max: getRuleValue(options.max),
    minLength: getRuleValue<number>(options.minLength) as number,
    maxLength: getRuleValue(options.maxLength) as number,
    pattern: getRuleValue(options.pattern) as string,
  } : {}),
  name,
  onChange,
  onBlur: onChange,
  ref: (ref: HTMLInputElement | null): void => {
    ...
  },
};

返回一个对象,包含字段的配置信息、事件处理器(如 onChangeonBlur)和 ref 回调函数。

4.1 处理 ref 回调
复制代码
ref: (ref: HTMLInputElement | null): void => {
  if (ref) {
    register(name, options);
    field = get(_fields, name);
    const fieldRef = isUndefined(ref.value) ? ref.querySelectorAll ? (ref.querySelectorAll('input,select,textarea')[0] as Ref) || ref : ref : ref;
    const radioOrCheckbox = isRadioOrCheckbox(fieldRef);
    const refs = field._f.refs || [];
    ...
  }
};

ref 回调用于处理实际的 DOM 元素引用。它将 DOM 元素的引用添加到字段的配置中,并更新字段的值和有效性状态。

5. 更新字段状态

复制代码
updateValidAndValue(name, false, undefined, fieldRef);

在注册字段时,更新字段的有效值和状态。

6. 处理字段卸载

复制代码
if (ref) {
  ...
} else {
  field = get(_fields, name, {});
  if (field._f) {
    field._f.mount = false;
  }
  (_options.shouldUnregister || options.shouldUnregister) && !(isNameInFieldArray(_names.array, name) && _state.action) && _names.unMount.add(name);
}

如果 refnull,则标记字段为未挂载,并将其添加到 _names.unMount 集合中,以便后续清理。

React Hook Form 的 register 函数设计巧妙之处在于它将表单字段的注册和状态管理封装得非常简洁和高效,同时提供了强大的功能和灵活性。

React Hook Form 如何实现更少的渲染

React Hook Form 通过智能的依赖追踪和高效的状态管理,实现了避免不必要渲染的优化。结合源码分析,我们可以深入了解其背后的实现机制。下面逐步解析前面提到的几点优化方式。

1. 依赖跟踪和字段级别的订阅

React Hook Form 的核心优化之一是字段级别的依赖追踪,只有在字段状态发生变化时才会重新渲染相关字段。

源码分析:

在 React Hook Form 的源码中,useForm 钩子通过 watch 函数来追踪每个字段的变化。watch 会订阅特定的表单字段,当这些字段的值或验证状态发生变化时,触发渲染。

useForm 中,字段的注册是在 register 函数中实现的。register 函数内部会将字段的 ref 存储在 fieldsRef 对象中。watch 函数通过 fieldsRef 来订阅特定字段的变化。

具体代码片段如下(简化):

复制代码
const useForm = () => {
  const fieldsRef = useRef({});
  
  const register = (name, rules) => {
    fieldsRef.current[name] = { rules, ref: null };
  };

  const watch = (name) => {
    // 订阅字段变化并返回当前值
    return fieldsRef.current[name].value;
  };

  return {
    register,
    watch,
  };
};

通过这种字段级别的订阅机制,React Hook Form 能够追踪到特定字段的变化,并且仅在相关字段更新时触发渲染,而不是整个表单。

2. 减少内部状态存储

React Hook Form 通过 useRef 而不是 useState 来存储字段的状态。这意味着 React 不会每次都因为状态更新而重新渲染组件。

源码分析:

在 React Hook Form 的 useForm 钩子内部,表单字段的状态和验证信息都存储在 useRef 中,而不是 useState。例如,fieldsReferrorsRef 都使用 useRef 来存储字段引用和错误信息。

这是一个重要的性能优化,因为 useRef 不会在更新时触发组件重新渲染。例如:

复制代码
const fieldsRef = useRef({});
const errorsRef = useRef({});

这样,表单字段的状态更新不会导致组件的重新渲染,只有在验证或提交时才会触发必要的状态更新。

3. 使用 Controller 组件优化受控组件

对于受控组件,React Hook Form 提供了 Controller 组件,它将受控组件的状态和表单管理的逻辑解耦,从而避免每次表单值变化都触发重新渲染。

源码分析:

Controller 的实现原理是通过 render 属性将受控组件的渲染逻辑分离,并通过 field 对象来管理表单的输入、值和验证。这种方式将表单状态的变更与 React Hook Form 的内部状态独立开来,从而减少不必要的渲染。

Controller 的核心代码片段如下(简化):

复制代码
const Controller = ({ name, control, render }) => {
  const { register, setValue, getValues } = control;

  // 注册表单字段并设置初始值
  const field = {
    onChange: (value) => setValue(name, value),
    value: getValues(name),
  };

  return render({ field });
};

Controller 只会在字段的值或验证状态发生变化时触发 setValue,而不会影响其他表单字段的状态更新,进一步减少了不必要的渲染。

4. 通过 shouldUnregister 控制字段的卸载行为

当表单字段被隐藏或移除时,React Hook Form 提供了 shouldUnregister 属性来控制字段的状态是否保留。如果设置为 false,字段的状态不会被移除,从而避免重复渲染和状态初始化。

源码分析:

useForm 钩子中,unregister 函数负责移除某个字段的状态。如果 shouldUnregister 被设置为 false,则字段的状态会保留在 fieldsRef 中,避免字段卸载和重新加载时的状态丢失及重新渲染。

以下是 unregister 函数的简化代码:

复制代码
const unregister = (name, shouldUnregister) => {
  if (shouldUnregister) {
    delete fieldsRef.current[name];
  }
};

shouldUnregister 控制了字段的状态存储和移除行为,从而减少了由于字段移除导致的不必要渲染。

5. 精细化错误处理机制

React Hook Form 的验证机制仅在字段值发生变化时才触发,且只会更新发生错误的字段的状态,从而减少整个表单重新渲染的可能。

源码分析:

错误处理机制依赖于 errorsRef,这是一个存储验证错误信息的 useRef 对象。每次表单提交或字段失效时,validateField 函数会更新 errorsRef,并且只会渲染那些有验证错误的字段。

复制代码
const validateField = (field) => {
  const error = validate(field);
  if (error) {
    errorsRef.current[field.name] = error;
  }
};

通过这种错误管理方式,React Hook Form 避免了全局重新渲染,仅更新有错误的字段。

以下是一个示例,展示了 React Hook Form 如何避免不必要的渲染:

复制代码
import React from 'react';
import { useForm, Controller } from 'react-hook-form';

const MyForm = () => {
  const { control, handleSubmit, formState: { errors } } = useForm();
  
  const onSubmit = data => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        name="firstName"
        control={control}
        defaultValue=""
        render={({ field }) => (
          <input {...field} />
        )}
      />
      {errors.firstName && <p>First name is required</p>}
      
      <Controller
        name="lastName"
        control={control}
        defaultValue=""
        render={({ field }) => (
          <input {...field} />
        )}
      />
      {errors.lastName && <p>Last name is required</p>}
      
      <button type="submit">Submit</button>
    </form>
  );
};

在这个例子中,Controller 组件只会在输入值发生变化时更新相应的字段,而不会导致整个表单重新渲染。这种按需渲染的方式使得 React Hook Form 在处理大型表单时具备良好的性能表现。

总结

React Hook Form 提供了一种强大且灵活的方式来处理表单,它主要关注于表单逻辑,而不会过多干涉表单的外观设计。这样,开发者可以使用任何喜欢的UI库或样式来构建表单,同时享受React Hook Form带来的便捷状态管理和验证功能。简而言之,它让表单逻辑变得简单,而让UI设计保持自由。

本文由mdnice多平台发布

相关推荐
百万蹄蹄向前冲1 小时前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
朝阳5812 小时前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路2 小时前
GeoTools 读取影像元数据
前端
ssshooter3 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
Jerry3 小时前
Jetpack Compose 中的状态
前端
dae bal4 小时前
关于RSA和AES加密
前端·vue.js
柳杉4 小时前
使用three.js搭建3d隧道监测-2
前端·javascript·数据可视化
lynn8570_blog4 小时前
低端设备加载webp ANR
前端·算法
LKAI.5 小时前
传统方式部署(RuoYi-Cloud)微服务
java·linux·前端·后端·微服务·node.js·ruoyi
刺客-Andy5 小时前
React 第七十节 Router中matchRoutes的使用详解及注意事项
前端·javascript·react.js