Formik 设计思路浅析

1,492 阅读8分钟

导读

背景的详细说明可以看《React Hook Form 设计思路浅析》这篇文章的背景部分,简单来说就是为了实现可配置化的表单组件,合理的设计其 API,对业内主流的表单状态管理工具进行调研,充分参考最优秀的设计思路,力求达到最优的设计方案。之前的调研结论请见《Form 表单状态管理工具调研》,本篇是对 Formik 设计思路的解析。

介绍

Formik 官网的介绍:

Formik is the world's most popular open source form library for React and React Native.

谷歌翻译:Formik 是世界上最受欢迎的 React 和 React Native 开源表单库。

再看一下经过笔者修改的基础 Demo 代码:

import React from "react";
import { Field, Formik } from "formik";

const MyInput = ({ field, form, ...props }) => {
  return <input {...field} {...props} />;
};

const formValidate = (values, props) => {
  const errors = {};

  if (!values.firstName) {
    errors.firstName = "firstName Required";
  }

  return errors;
};

const fieldValidate = (value) => {
  let errorMessage;
  if (!value) {
    errorMessage = "Required";
  } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(value)) {
    errorMessage = "Invalid email address";
  }
  return errorMessage;
};

const Example = () => (
  <div>
    <h1>My Form</h1>
    <Formik
      initialValues={{ email: "", color: "red", firstName: "", lastName: "" }}
      onSubmit={(values, actions) => {
        setTimeout(() => {
          alert(JSON.stringify(values, null, 2));
          actions.setSubmitting(false);
        }, 1000);
      }}
      validate={formValidate}
    >
      {(props) => (
        <form onSubmit={props.handleSubmit}>
          <Field name="email" validate={fieldValidate}>
            {({
              field, // { name, value, onChange, onBlur }
              form: { touched, errors }, // also values, setXXXX, handleXXXX, dirty, isValid, status, etc.
              meta,
            }) => (
              <div>
                <input type="email" placeholder="Email" {...field} />
                {meta.touched && meta.error && (
                  <div className="error">{meta.error}</div>
                )}
              </div>
            )}
          </Field>
          <Field as="select" name="color">
            <option value="red">Red</option>
            <option value="green">Green</option>
            <option value="blue">Blue</option>
          </Field>

          <Field type="firstName" name="firstName" placeholder="Trump" />
          {props.errors.firstName && <div>{props.errors.firstName}</div>}
          <Field name="lastName" placeholder="Donald" component={MyInput} />
          <button type="submit">Submit</button>
        </form>
      )}
    </Formik>
  </div>
);

export default Example;

从 Demo 中我们可以看到,Formik 的用法更偏向于 Render Props。React 在共享代码方案上,经历了 HOC => Render Props => Hooks 的过程,后面的方案较前面的总体来看都会更合理,更方便。

在查看了 Formik 的 API 后,发现果然有 hooks 的用法。进一步查看其 release 记录,发现 hooks 是在 2019.11 发布了 v2.0 版本之后才引入的。就笔者的目的而言,肯定是更倾向于使用数据与表象分离更彻底的 Hooks,而且重点是要分析表单状态处理的设计思路,所以本篇分析将以分析 Hooks 为主。

API 解析

主要的 hooks 有 useFormikuseFielduseFormikContext,再加上一个处理数组的组件 <FieldArray />,内容实际上不算多。其他的方法和组件基本上都可以用 hooks 替代,会在文章最后简单介绍。

useFormik(重要)& <Formik /> & useFormikContext

我们先将 <Formik /> 组件的 Demo 用 useFormik 实现,对比一下看看:

image.png

=============== 插播 ===============

有没有发现个问题:所有的 Field 都绑定 handleChange,它怎么分辨是哪个字段的?

引用官网的解释:

General input change event handler. This will update the values[key] where key is the event-emitting input's name attribute. If the name attribute is not present, handleChange will look for an input's id attribute.

所以你的 Field 必须有 name 或 id 属性。是不是觉得有点怪怪的?还是 Field 那种写法比较直观。

=============== 结束 ===============

从上例可以得出如下对应关系:

  • <Formik /> 组件的 props 相当于 useFormik 的入参;
  • <Formik /> 组件提供的 Render Props 上下文 相当于 useFormik 的返回值;

所以二者几乎是等价的。但是注意文档中的一句话:

Be aware that <Field><FastField><ErrorMessage>connect(), and <FieldArray> will NOT work with useFormik() as they all require React Context.

这句话引起了笔者的注意,其它的好说,如果 <FieldArray> 组件不能与 hooks 一起用,而且没有替代的 hook 的话,应该会有一定的功能缺陷。不过理论上实现 useFieldArray 这个 hook 应该不难,但是笔者没有想明白为什么没有提供这个 hook,毕竟 React Hook Form(下文简称 RHF) 是实现了的,这里先留一个疑问吧。

最后提一下 useFormikContext,它功能基本上跟 useFormik 一致,只是用来提供全局上下文的,方便嵌套深的组件使用,入参和返回值都可以参考 useFormik 这里就不做展开了。

返回值

说回 useFormik,其返回值可以参考 <Formik /> 的 Formik render methods and props 部分,主要包括:

  1. handleXXX 类的操作表单的函数,如 handleSubmit、handleChange 等;
  2. isXXX 类的状态变量,如 isSubmitting、isValidating 等;
  3. setXXX 类的操作表单输入组件的函数,如 setFiledValue、setStatus 等;
  4. 还有 valuesstatuserrors 等常用数据;

入参

入参可查看上述文档当中的其它部分,主要包括:

  1. onXXX 的回调函数定义,如 onSubmit、onReset 等;
  2. boolean 类型的功能开关,如 isInitialValid、validateOnChange 等;
  3. 还有 initialValuesvalidate 等常用初始化设置项;

由此可以看出 useFormik 基本上就是操作整个表单级别的功能和数据汇总,是 Formik 核心功能的很大一块。

useFiled(重要)& <Field /> & <FastField />

与 useFormik 类似,useField<Field /> 功能几乎一样,而 <FastField /> 本质上只是 <Field /> 的高性能版本,它只会在某几个 props 变化时触发重新渲染,文档描述如下:

For example, <FastField name="firstName" /> will only re-render when there are:

  • Changes to values.firstNameerrors.firstNametouched.firstName, or isSubmitting. This is determined by shallow comparison. Note: dotpaths are supported.
  • A prop is added/removed to the <FastField name="firstName" />
  • The name prop changes

When to use <FastField />

If a <Field /> is "independent" of all other <Field />'s in your form, then you can use <FastField /> .

所以 FastField 大致等价于 React.memo(Field, shouldUpdateFunc),用法上没有区别,所以我们只需要重点看 useField 的设计思路即可。来看下 Demo 的部分代码:

const MyTextField = ({ label, ...props }) => {
  const [field, meta, helpers] = useField(props);
  return (
    <>
      <label>
        {label}
        <input {...field} {...props} />
      </label>
      {meta.touched && meta.error ? (
        <div className="error">{meta.error}</div>
      ) : null}
    </>
  );
};

返回值

从 Demo 我们看到,其返回值主要有 fieldmetahelpers 三个对象,它们在文档中分别对应 FieldInputProps<Value>FieldMetaProps<Value>FieldHelperProps,具体如下:

  • FieldInputProps,主要包括 nameonBluronChangevalue,这与 RHF useController 的返回值 field 仅差一个 ref;
  • FieldMetaProps,主要包括 errorinitialValuetouchedvalue 等表示状态的值,部分类似于 RHF useController 的返回值 fieldState
  • FieldHelperProps,包括 setValuesetTouchedsetError 3 个方法,也就是表单输入组件级别的主动操作函数。

入参

入参主要就 2 个,namevalidate,其它可忽略,通常只需要传入 name 即可。

<FiledArray />

直接上 Demo:

import React from 'react';
import { Formik, Form, Field, FieldArray } from 'formik'

export const FriendList = () => (
  <div>
    <h1>Friend List</h1>
    <Formik
      initialValues={{ friends: ['jared', 'ian', 'brent'] }}
      onSubmit={...}
      render={formikProps => (
        <FieldArray
          name="friends"
          render={({ move, swap, push, insert, unshift, pop }) => (
            <Form>
              {/*... use these however you want */}
            </Form>
          )}
        />
    />
  </div>
);

用法比较简单,就是在 render 中写组件就行了,关键是看其提供的方法,也就是 arrayHelpers,有 move、swap、push、insert 等,一看就是用来处理数组的。跟 RHF 的 useFieldArray 相似度极高,只是不是 hook。这也正是笔者有疑问的地方,上文也已经提过,Render Props 都实现了,hook 的实现应该很容易才对。如果是依赖 context,不是还有 useFormikContext 吗?或者将 context 作为 hook 的参数传入也可以啊,虽然不太优雅。

笔者简单搜了一下 issues,发现作者似乎在 2021 年有尝试过添加这个 hook,但是最终也没有结果,因为对笔者要研究的问题没有太大影响,所以就不再花费精力深究了,反正笔者会选择实现这个 hook。

Others

  • connect 是个 HoC,可以将 Formik 的上下文注入到被包裹的组件的 props 中,可用 useFormikContext 替代;
  • <ErrorMessage /> 本质就是一个方便显示 error 的组件,看代码更好理解一些;
{errors.name && touched.name ? (
  <div>{errors.name}</div>
) : null}
/* 等价于 */
<ErrorMessage name="name" />
  • <Form /> 本质是对原生 form 进行的封装,自动注入了 handleSubmit 和 handleReset 方法,看代码比较好理解;
<Form />
/* 等价于 */
<form onReset={formikProps.handleReset} onSubmit={formikProps.handleSubmit} {...props} />
  • withFormik 本质也是个 HoC,还是为普通组件的 props 中注入 Formik 的能力,可以用 useFormikContext 替代,详情见文档。

以上概念不是在 Hooks 下没有太大作用,就是封装的组件,所以就不展开了。不过再多一句嘴,因为 Hooks 而让这么多概念失效,也算是侧面体现了 Hooks 的先进性。

总结

Formik vs. React Hook Form

整体来看,Formik 的核心就是 useFormikuseFieldFiledArray 组件,分别处理表单组件、表单输入组件、数组类字段。分别对应了 RHF 中的 useFormuseControlleruseFieldArray。除此之外,RFH 还有 useWatch、useFormState,不过这两个 hooks 都可以被 useForm 中的返回的 watch、formState 替代,甚至 useController 都可以被 register 一定程度的替代,所以说 RHF 的核心是这三个 hooks 也没什么大毛病。

二者最大的不同,RHF 是纯 Hooks 的框架,Formik 更偏向 Render Props。毕竟现在是 Hooks 的时代,二者的下载量已经几乎持平了,GitHub 上的 Stars 数 RHF 也是增长强劲,相信很快 RHF 的数据会全面超过 Formik。

不过不管 Hooks 还是 Render Props 对于设计理念来说是没什么影响的。总体来说,只要处理好表单(Form)级、表单输入组件(Field)级的状态管理,以及解决好数组类字段 这个特殊的点,表单状态管理问题基本就能够解决了。

落地到细节,还有 formState、validate、watch(组件联动)的具体实现,不过这些都是要依赖 Form 和 Field 级别状态管理的实现。

最后还有一些需要枚举的方法和属性,比如主动操作类的方法 setXXX、各种回调的注册 onXXX、根据回调生成的操作函数 handleXXX、还有一些 boolean 类型的开关,这些基本可以照搬过来

更进一步

学习完这两个头部的框架后,对于表单状态管理的理解可以说更进了一步,可惜二者都是 React 系。Vue 系暂时没有找到相匹配的框架,也许 Vue 中 v-model 的设计让表单状态管理表面上更加方便,所以就不需要专门再实现表单状态管理的库了。但是,随着 Vue3 的铺开,Hooks 的理念会越来越影响到 Vue 阵营,笔者大胆推测 Vue3 的表单状态管理库肯定会有不一样的变化的。

还剩下一个 final-form,初看起来是一个框架无关的表单状态管理库,这也是笔者将其纳入调研计划的一个重要原因。希望在学习完它之后,能够补上框架无关的这块拼图。

“在激烈竞争中,取胜的系统在最大化或者最小化一个或几个变量上会走到近乎荒谬的极端。”

"In the fierce competition, the winning system will go to the absurd extreme in the maximization or minimization of one or several variables"——Charlie Thomas Munger