skip to content
pureink

「Form」为什么要使用表单库

/ 10 min read

目录

前言

表单是前端开发中常见的场景,注册登录、调查问卷,乃至租赁一个云服务器都是在填写表单。

本文根据我的表单开发经验分享表单开发中,为什么需要表单库进行辅助。

原生表单

前端技术上,表单对应form标签,MDN有详细的介绍。

以一个注册表单为例,我们需要如下标签

这离用户体验还很远,想要完善它的功能,都要依靠JS,原生表单有很多能力缺口,例如:

  1. 无法展示错误信息

为了告知用户错误的原因,常见在输入框底部进行红色文案的提示,如下图。HTML会给校验失败的元素添加CSS 伪类 :invalid ,但这还远远不够。 错误展示

  1. 不支持自定义校验

内置支持require、pattern正则这些能力,但确认密码的校验需要和第一次填写的密码进行对比,没有相关的属性可以用。

React中的表单

尝试在React下实现一个完整的注册表单。

import { useState } from "react";
import "./App.css";

function App() {
  const [formValue, setFormValue] = useState({
    name: "",
    password: "",
    confirm_password: "",
  });

  const [error, setError] = useState({
    name: "",
    password: "",
    confirm_password: "",
  });

  const validators = {
    name: (value) => (value.length === 0 ? "用户名不能为空" : ""),
    password: (value) => (value.length === 0 ? "密码不能为空" : ""),
    confirm_password: (value) =>
      value !== formValue.password ? "两次输入的密码不同" : "",
  };

  const changeValue = (e) => {
    const { name, value } = e.target;
    setFormValue({
      ...formValue,
      [name]: value,
    });
    setError({
      ...error,
      [name]: validators[name](value),
    });
  };

  const validateAll = () => ({
    name: validators.name(formValue.name),
    password: validators.password(formValue.password),
    confirm_password: validators.confirm_password(formValue.confirm_password),
  });
  
  const handleSubmit = (e) => {
    e.preventDefault();
    const newErrors = validateAll();
    const hasError = Object.values(newErrors).some((err) => err !== "");
    if (!hasError) {
      alert(JSON.stringify(formValue, null, 2))
    }
    setError(newErrors);
  };
  return (
    <>
      <form onSubmit={handleSubmit}>
        <label htmlFor="name">用户名</label>
        <input
          name="name"
          value={formValue.name}
          onChange={changeValue}
        ></input>
        <div className="error">{error.name}</div>
        <label htmlFor="password">密码</label>
        <input
          name="password"
          value={formValue.password}
          onChange={changeValue}
        ></input>
        <div className="error">{error.password}</div>
        <label htmlFor="confirm_password">确认密码</label>
        <input
          name="confirm_password"
          value={formValue.confirm_password}
          onChange={changeValue}
        ></input>
        <div className="error">{error.confirm_password}</div>
         <input type="submit" />
      </form>
    </>
  );
}

export default App;

我们维护了两个状态对象,一个formValue代表表单值,一个error用于渲染错误信息。每一个input输入框传入value与onChange使其受控,用户输入后会修改formValue并进行校验。同时提交时也会重新进行校验,满足条件会打印数据。

表单库

如果借助表单库会更简单么?使用React Hook Form,代码如下

import { useForm } from "react-hook-form";
import { ErrorMessage } from "@hookform/error-message";
import "./App.css"

const ErrorRender = ({ message }) => <div className="error">{message}</div>;

function App() {
  const { register, handleSubmit, getValues, formState: { errors } } = useForm();
  const onSubmit = (data) => alert(JSON.stringify(data, null, 2));
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label htmlFor="name">用户名</label>
      <input {...register("name", { required: "用户名不能为空" })} />
      <ErrorMessage errors={errors} name="name" render={ErrorRender} />
      <label htmlFor="password">密码</label>
      <input
        {...register("password", { required: "密码不能为空" })}
      />
      <ErrorMessage errors={errors} name="password" render={ErrorRender} />
      <label htmlFor="confirm_password">确认密码</label>
      <input
        {...register("confirm_password", {
          validate: (value) => value !== getValues("password") ? "两次输入的密码不同" : true })}
      />
      <ErrorMessage errors={errors} name="confirm_password" render={ErrorRender} />
      <input type="submit" />
    </form>
  );
}

export default App;

可以看到只用了不到一半的代码实现了相同的功能。

表单库都做了哪些事情

从上面一个简单的例子可以看出表单库可以大大简化代码的编写,上述例子主要体现状态管理(formState和error)以及校验,下面将更体系介绍一个表单库可以提供的能力。

一、状态管理

  1. 表单值集中管理

表单在提交时需要上传一个对象,如

{
"name": "pureink",
"password": "fakePassword",
"confirm_password": "fakePassword",
}

在上面的例子中,我们创建了一个formState状态,为每一个输入框绑定了值,同时在onChange时修改formState,表单库将接管这些状态的管理。

我们可以以一个整体对该数据进行处理,例如可以给表单赋予初始值

const { register, getFieldState, formState: { errors } } = useForm({
defaultValues: {
name: "张三",
},
})
  1. 辅助状态管理

表单中最重要的状态是值,但我们还需要其他状态辅助渲染

例如:

touched:一个全新的表单,在用户填写前,即使输入框校验为空,也不应该展示任何错误消息,touched可以帮助我们判断用户是否有输入操作

isValidating:对于异步校验的表单,你可能希望在校验时禁止用户的输入

error:校验错误的信息,例如渲染在输入框下的红色文案

类似的属性还有active、dirty、visited等等,对于每一个表单项,表单库都会管理以上状态。

  1. 值清空和重置

问卷中常见B问题只在A问题选中某一项时展示,对于会显示和隐藏的表单项,表单库可以处理其值的清空和重置。

下面例子中,勾选问题1后,填写问题2。此时取消勾选问题1并提交,将不会看到问题2的值。

import { useForm } from "react-hook-form";
import "./App.css";

function App() {
  const {
    register,
    handleSubmit,
    getValues,
    formState: { errors },
    watch
  } = useForm({
     shouldUnregister: true
  });
  const onSubmit = (data) => alert(JSON.stringify(data, null, 2));
  const watchQ1 = watch("q1")
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label htmlFor="q1">你是否使用过React?</label>
     <input
       {...register("q1")}
        id="lastNameCheck"
        type="checkbox"
      />
      {watchQ1 === true && (
        <>
          <label htmlFor="q2">你觉得React怎么样?</label>
          <input {...register("q2")} defau />
        </>
      )}
      <input type="submit" />
    </form>
  );
}

export default App;

(也支持不清空,例子中可以设置shouldUnregister为false)

二、校验

  1. 自定义校验

不受限于HTML的required和pattern,你可以编写任意的函数进行同步或异步的校验。

  1. 校验策略

支持表单级别校验(Form-Level),字段级别校验(Field-Level)

所有表单库都支持字段级别校验,你可以对每个字段指定校验规则,细粒度的进行控制

部分表单库提供表单级别校验,对于简单表单,可以简化校验的编写,如Final-Form

<Form
onSubmit={onSubmit}
validate={values => {
const errors = {}
if (!values.username) {
errors.username = 'Required'
}
if (!values.password) {
errors.password = 'Required'
}
if (!values.confirm) {
errors.confirm = 'Required'
} else if (values.confirm !== values.password) {
errors.confirm = 'Must match'
}
return errors
}}>xxx</Form>
  1. 校验集成

如React-Hook-Form、Formik可以与Yup、Zod这样的校验规则库结合使用

const schema = yup
.object({
firstName: yup.string().required(),
age: yup.number().positive().integer().required(),
})
.required()
  1. 校验时机

触发校验的时机不只是onChange, 例如注册表单中你可能需要发起请求判断用户名称是否重复,使用onChange可能会校验多次,可以选择blur(失焦)事件触发时校验。

例如Final-Form和Formik都支持validateOnBlur参数。

  1. 主动触发

例如注册表单中,输入了同样的密码和确认密码,此时如果再次更改密码,确认密码的校验并不会运行,因为并没有对它进行直接的更改,触发其onChange事件。

表单库支持主动触发一个字段的校验,如React Hook FormFormik

Final-Form使用了另一种方式支持这种场景,本文不进行深入。

  1. 提交联动

表单进行提交时,需要对每一项内容进行校验,一些表单库还会提供滚动、focus到第一个错误表单项的能力。

  1. 渲染

一些表单库如Formik可以和第三方组件库如Antd简单结合,错误时输入框会变红等。

三、联动

多字段表单中各个字段经常互相关联,例如选择地区时,需要根据省市区的顺序进行选择,前一个选择影响后面的可选项。

你可以选择自定义onChange方法,当值变更时进行操作,也可以使用useEffect触发。

Formily这样的库支持一对一、一对多、多对一等方式的联动处理,开发效率和维护性都更高。

四、其他

  1. 性能

使用React Context管理表单值会导致任何一个表单项值变化,整个表单都会重新渲染一遍。表单库提供监听的功能,避免重复的渲染。

如Formik的FastField,Final Form的subscription

  1. 表单提交处理

除了可以直接拿到整个表单的值、触发整体校验以外,你可以感知表单是否在提交展示Loading动画,或禁用输入框避免用户此时输入等。

  1. 数组或嵌套字段

对于复杂字段的处理,如Final Form对于数组能力的支持

import { createForm } from 'final-form'
import arrayMutators from 'final-form-arrays'
// Create Form
const form = createForm({
mutators: { ...arrayMutators },
onSubmit
})
// push
form.mutators.push('customers', { firstName: '', lastName: '' })
// pop
const customer = form.mutators.pop('customers')
  1. 动态化配置

如Formily这样完备的库支持JSON Shcema形式配置表单。

总结

表单因为多字段下状态管理和校验复杂导致代码量很多,借助表单库可以提升很多效率。

了解一个库的设计目的才能更好的使用工具,本文旨在说明为什么需要一个表单库 / 表单库做了什么事情。

下一篇文章会对React生态下流行的表单库进行多维度对比。