ZHANGYU.dev

October 14, 2023

利用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 schemaconst 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的源码,发现基本能看懂了,仔细回顾这一年的成长还是挺多的

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