ZHANGYU.dev

October 14, 2023Last Updated: April 4, 2024

利用Formik解决表单痛点

JavaScriptReact6.0 min to read

一直觉得表单问题是非常复杂,如何能够优雅的获取表单的值,又如何能优雅的展示不同的报错信息?

幸好现在是框架时代,如果用jQuery来做,想想也复杂

以前写PC端页面,多是管理系统,表单的处理非常的简单了,因为有ant design,我觉得ant-design在后台管理的ui框架中,是王者级别的,它的处理非常的简单,表单错误信息也展现的如此的完美

但是最近一直在做微信端的页面,自然用不了ant-design,表单验证就比较复杂,刚开始不知道天高地厚,自己造了个useForm的轮子,博客也有记录,就是一个非常简单的轮子,后来功能越来越多,也就越改越乱,索性放弃了

后来才发现,其实针对表单问题,在github有非常多的解决方案,其中在我发现的方案中,star最多的是formik

Formik

formik的描述是

Build forms in React, without the tears 😭

它主要解决3个问题

  1. form状态树的内部或外部获取数据
  2. 验证并输出错误信息
  3. 处理表单的提交

使用formik后,整个表单的状态就由它接管了,valuessubmiterrors,表单组件onChange都被接管,也控制整个表单的更新

formik主要有2种使用方式,一种是render props,另一种则是趋势之hooks,它的状态保存使用的是ReactContext

render props示例

一个简单的示例

<Formik
    initialValues={{ name: "zy" }}
    onSubmit={values => console.log(values)}
>
    {({ handleSubmit, handleChange, handleBlur, values, errors }) => (
        <form onSubmit={handleSubmit}>
            <input
                type="text"
                onChange={handleChange}
                onBlur={handleBlur}
                value={values.name}
                name="name"
            />
            {errors.name && <div>{errors.name}</div>}
            <button type="submit">Submit</button>
        </form>
    )}
</Formik>

Formik组件接收2个必需的props

  1. initialValues 表单的初始值
  2. onSubmit 表单的提交事件

对于表单控件值和控制的判断是根据表单控件的name属性

render props其实就是把props.children当作函数调用,传入的props非常多,举几个常用的

  1. handleSubmit: (e: React.FormEvent) => void 提交表单,formik会判断仅当表单数据通过验证的时候才会调用函数

  2. handleReset: () => void 重置表单

  3. handleChange: (e: React.ChangeEvent) => void 表单控件onChange函数

  4. handleBlur: (e: any) => void 表单控件onBlur失焦函数,如果需要使用touched,这个函数是必要的

  5. values: { [field: string]: any } 表单的值

  6. touched: { [field: string]: boolean } 表单控件是否被点击过

  7. errors: { [field: string]: string } 表单的错误信息

  8. isSubmitting: boolean 表单提交事件是否正在pendingformik会等待传入的onSubmit事件执行完毕,这个值会从函数开始调用 -> true -> 调用结束 -> false,很方便的用在loading状态的展示

formik控制自定义组件

最简单的方式,就是将需要的值value和改变值的handleChange作为props传入组件,不过这种方式就很笨,formik也同样提供了render propshooks两种方式

Field

render props的方式,不多介绍

useField()

hooks的方式,一个简单的例子

const TextField = (props) => {
    // 返回 <input /> 需要的props
    const [field, meta] = useField(props.name);
    return (
        <>
            <input {...field} {...props} />
            {meta.error && meta.touched && <div>{meta.error}</div>}
        </>
    );
}

useField接受一个参数,即是表单控件的name属性,formik是根据name来判断哪一个组件对应哪一个值的,这个name值必须和传入<Formik/>initialValues的一个字段相同

它返回一个2个元素的数组,分别是FieldPropsFieldMetaProps,以下分别列举几个常用的

FieldProps

FieldMetaProps

表单的验证

表单的验证也是一个比较麻烦的事情,因为一个字段可能有多种错误信息,如至少4位最多6位不能为空

formik也提供了验证propsvalidate,接收一个函数,这个函数接受1个必要参数values,返回一个errors对象,如果errors对象里的键名和表单字段对应,则会将error传给对应的表单控件

<Formik
  initialValues={{ password: "" }}
  validate={values => {
    const errors = {};
    if (!values.password) {
      errors.password = "不能为空";
    } else if (values.password.length < 4) {
      errors.password = "至少4位";
    } else if (values.password.length > 6) errors.password = "最多6位";
    return errors;
  }}
  onSubmit={console.log}
  >
  ...
</Formik>

如上所示,如果使用if-else来完成多种错误信息的判断,那代码也太冗长了,一旦有变化,修改起来也很麻烦,好在formik内置了一个非常不错的验证方案yup,这里不再多做介绍,直接看例子

// yup schema
const schema = object().shape({
    password: string()
        .min(4, "至少4位")
        .max(6, "最多6位")
        .required("不能为空")
});

// Formik
  <Formik
    initialValues={{ password: "" }}
    validationSchema={schema}
    onSubmit={console.log}
  >
      ...
  </Formik>
);

与自己写验证函数不同的是,使用yup对应的propvalidationSchema

使用这些强大的库,一切都变得美妙了起来

写一个简单的例子

介绍了如何使用<Formik/>来控制表单状态和使用useField来进行组件分离,下面写一个简单的例子

自定义input组件

const TextField = ({ name, label, type = "text", ...props }) => {
    const [field, meta] = useField(name);
    const error = meta.error && meta.touched;
    return (
        <div className="text-field">
            <label className="text-field-label">
                <div className="text-field-name">{label}</div>
                <input
                    className={error ? "error" : undefined}
                    name={name}
                    type={type}
                    value={field.value}
                    onChange={field.onChange}
                    onBlur={field.onBlur}
                    {...props}
                />
            </label>
            {error && <div className="text-field-error">{meta.error}</div>}
        </div>
    );
};

使用yup来验证

const loginSchema = object().shape({
    phone: string()
        .length(11, "请输入11位手机号")
        .required("请输入手机号"),
    password: string()
        .min(4, "密码至少4位")
        .max(6, "密码最多6位")
        .required("请输入密码"),
});

使用Formik

const FormikExample = () => (
    <Formik
        initialValues={{ phone: "", password: "" }}
        onSubmit={values =>
            new Promise(resolve =>
                setTimeout(() => resolve(alert(JSON.stringify(values))), 1000)
            )
        }
        validationSchema={loginSchema}
    >
        {({ handleSubmit }) => (
            <form onSubmit={handleSubmit} className="formik-example-form">
                <TextField label="手机号" name="phone" />
                <TextField label="密码" name="password" />
                <button className="formik-submit" type="submit">
                    提交
                </button>
            </form>
        )}
    </Formik>
);

onSubmit函数如果返回一个Promise或者是async/await函数,formik会自动处理isSubmitting,如果使用异步函数的话,还需要接收第二个参数,里面有一个setSubmitting来手动修改提交状态

总结

这里介绍的方法只是Formik的一小部分,运用这一小部分已经能完成很多功能了

yup是个非常简洁的库,但是研究下来,好像没有能直接把错误信息作为errors对象输出的方法,就连Fomrik也是for循环一个一个取的

除了Formik,还有不少方案,如react-final-form,这个我也使用过,感觉上实现方法大致可能是相同的,不过它像redux一样,分为了final-formreact-final-form,也就是一个通用的方案,感觉还是很不错,它不像Formik一样,必须设置初始值,感觉设置初始值大多数时候都是多余的

另外有一个轻量化的react-final-form-hooks,它没有使用Context API

ant design的方案我还没有研究过,它使用的验证方案是**async-validator**,我也没研究过😂,有空还需要多研究,记得一年前的我想看ant design的源码,发现基本看不懂,最近看了ButtonInput的源码,发现基本能看懂了,仔细回顾这一年的成长还是挺多的

我觉得表单的性能优化也是一部分,不过基本没有考虑过这些,哎,每次使用这些方便好使的库,就感觉开发者很牛皮,我也想成长啊!