本篇文章将带您从了解formik到掌握,最后熟练使用。本篇文章的code您可以在gitee中获取:https://gitee.com/xifeng-canyang/jest-formik/tree/master/src/page/formik
- 何为formik
 - 与Redux-Form比较
 - 安装
 - show Time
- 基本例子
 - 校验
 - yup校验
 - Formik组件
 - array or object formik
 - 常用api
- ErrorMessage
 - Field
 - getIn()
 - setValues()和setFieldValue()
 
 
 - 案例
- 来一个简单的
 - 再来一个难一点的
 
 
何为formik
什么是formik呢?Formik 是一小群 React 组件和钩子,用于在 React 和 React Native 中构建表单。它有助于解决三个最烦人的部分,帮助我们解决了表单中的三个核心问题:
- 从表单状态获取值和从表单状态获取值
 - 验证和错误消息
 - 处理表单提交
由此可知,Formik 将使事情井井有条——使测试、重构和推理表单变得轻而易举。 
与Redux-Form比较
- Redux-Form 在每次按键时都会多次调用整个顶级 Redux 减速器。这对于小型应用程序来说很好,但是随着您的 Redux 应用程序的增长,如果您使用 Redux-Form,输入延迟将继续增加。
 - Redux-Form 压缩压缩后大小为 22.5 kB(Formik 为 12.7 kB)
 - Formik 的目标是创建一个可扩展、高性能的表单助手
 
安装
依赖包安装
npm install formik --save
cdn集成
<script src="https://unpkg.com/formik/dist/formik.umd.production.min.js"></script>
添加此内容后,您将可以访问变量window.Formik<Insert_Component_Name_Here>。
show Time
基本例子
先来看一个简单的表单例子,新建文件src\page\formik\com\basic-example.tsx:
import React from 'react';
import { useFormik } from 'formik';
import { validate1 } from './common/utils';
const SignupForm = () => {
  // Pass the useFormik() hook initial form values and a submit function that will
  // be called when the form is submitted
  const formik = useFormik({
    initialValues: {
      email: '',
      firstName: '',
      lastName: '',
    },
    validate: validate1,
    onSubmit: values => {
      alert(JSON.stringify(values, null, 2));
    },
  });
  return (
    <form onSubmit={formik.handleSubmit}>
        <label htmlFor="firstName">First Name</label>
        <input
            id="firstName"
            name="firstName"
            type="text"
            onChange={formik.handleChange}
            value={formik.values.firstName}
        />
        {formik.errors.firstName ? <div>{formik.errors.firstName}</div> : null} <br /> 
        <label htmlFor="lastName">Last Name</label>
        <input
            id="lastName"
            name="lastName"
            type="text"
            onChange={formik.handleChange}
            value={formik.values.lastName}
        />
        {formik.errors.lastName ? <div>{formik.errors.lastName}</div> : null} <br /> 
        <label htmlFor="email">Email Address</label>
        <input
            id="email"
            name="email"
            type="email"
            onChange={formik.handleChange}
            value={formik.values.email}
        />
      
        {formik.errors.email ? <div>{formik.errors.email}</div> : null} <br /> 
        <button type="submit">Submit</button>
    </form>
  );
};
export default SignupForm;
解释:
- handleSubmit:提交处理程序
 - handleChange<input>:传递给每个、<select>、 或的更改处理程序<textarea>
 - values:我们表单的当前值
 - handleChange我们为每个 HTML 输入重用相同的更改处理函数
 - 我们传递一个id与我们定义的属性相匹配的nameHTML 属性initialValues
 - 我们使用相同的名称(email-> formik.values.email)访问字段的值
假如我们没有认识formik这个东西,那么我们监测受控组件就得一个个写change事件。 
校验
Formik 不仅跟踪您的表单values,还跟踪其验证和错误消息。要使用 JS 添加验证,让我们指定一个自定义验证函数并将其传递给validate钩子useFormik()。如果存在错误,这个自定义验证函数应该生成一个与我们的/error形状匹配的对象。
在前面的例子basic-example.tsx当中,我们引入了一个validate的文件,因此,我们需要新建文件:src\page\formik\com\common\utils.ts
export const validate1 = (values: any) => {
    const errors = {} as any;
    if (!values.firstName) {
      errors.firstName = 'Required';
    } else if (values.firstName.length > 15) {
      errors.firstName = 'Must be 15 characters or less';
    }
    if (!values.lastName) {
      errors.lastName = 'Required';
    } else if (values.lastName.length > 20) {
      errors.lastName = 'Must be 20 characters or less';
    }
    if (!values.email) {
      errors.email = 'Required';
    } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)) {
      errors.email = 'Invalid email address';
    }
    return errors;
};
默认情况下,Formik 将在每次击键(更改事件)、每个输入的模糊事件以及提交之前进行验证。校验确实实现了,但是如果我们希望在提交表单的时候再显示错误,需要怎么做呢?
Formik 会跟踪哪些字段已被访问过。它将这些信息存储在一个名为 touched 的对象中,同时也反映了values/initialValues的值,这些值为boolean值。
为了利用touched,我们传递formik.handleBlur给每个输入的onBlurprop。此函数的工作原理与它类似formik.handleChange,它使用name属性来确定要更新那个字段。
我们新建文件src\page\formik\com\basic-example-touched.tsx
import React from 'react';
import { useFormik } from 'formik';
import { validate1 } from './common/utils';
const SignupForm = () => {
  // Pass the useFormik() hook initial form values and a submit function that will
  // be called when the form is submitted
  const formik = useFormik({
    initialValues: {
      email: '',
      firstName: '',
      lastName: '',
    },
    validate: validate1,
    onSubmit: values => {
      alert(JSON.stringify(values, null, 2));
    },
  });
  return (
    <form onSubmit={formik.handleSubmit}>
        <label htmlFor="firstName">First Name</label>
        <input
            id="firstName"
            name="firstName"
            type="text"
            onChange={formik.handleChange}
            // onBlur={formik.handleBlur}
            value={formik.values.firstName}
        />
            {formik.touched.firstName && formik.errors.firstName ? (
                <div>{formik.errors.firstName}</div>
            ) : null}  <br />
        <label htmlFor="lastName">Last Name</label>
        <input
            id="lastName"
            name="lastName"
            type="text"
            onChange={formik.handleChange}
            // onBlur={formik.handleBlur}
            value={formik.values.lastName}
        />
        {formik.touched.lastName && formik.errors.lastName ? (
            <div>{formik.errors.lastName}</div>
        ) : null} <br />
        <label htmlFor="email">Email Address</label>
        <input
            id="email"
            name="email"
            type="email"
            onChange={formik.handleChange}
            // onBlur={formik.handleBlur}
            value={formik.values.email}
        />
      
        {/* {formik.errors.email ? <div>{formik.errors.email}</div> : null} */}
        {formik.touched.email && formik.errors.email ? (
            <div>{formik.errors.email}</div>
        ) : null} <br />
        <button type="submit">Submit</button>
    </form>
  );
};
export default SignupForm;
yup校验
由于 Formik 作者/用户非常喜欢Yup,因此 Formik 有一个名为 Yup 的特殊配置道具validationSchema,它会自动将 Yup 的验证错误消息转换为一个漂亮的对象,其键匹配values
安装
npm install yup --save
我们新建文件:src\page\formik\com\basic-validate.tsx
import React from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';
const SignupFormValidate = () => {
  // Pass the useFormik() hook initial form values and a submit function that will
  // be called when the form is submitted
  const formik = useFormik({
    initialValues: {
      email: '1@qq.com',
      firstName: 'david',
      lastName: 'zhu',
    },
    validationSchema: Yup.object({
      firstName: Yup.string()
        .max(15, 'Must be 15 characters or less')
        .required('Required'),
      lastName: Yup.string()
        .max(20, 'Must be 20 characters or less')
        .required('Required'),
      email: Yup.string().email('Invalid email address').required('Required'),
    }),
    onSubmit: values => {
      alert(JSON.stringify(values, null, 2));
    },
  });
  return (
    <form onSubmit={formik.handleSubmit}>
        <label htmlFor="firstName">First Name: </label>
        <input
            id="firstName"
            name="firstName"
            type="text"
            onChange={formik.handleChange}
            onBlur={formik.handleBlur}
            value={formik.values.firstName}
            //{...formik.getFieldProps('firstName')}
        />
        {formik.touched.firstName && formik.errors.firstName ? (
            <div>{formik.errors.firstName}</div>
        ) : null}  <br />
        <label htmlFor="lastName">Last Name: </label>
        <input
            id="lastName"
            name="lastName"
            type="text"
            onChange={formik.handleChange}
            onBlur={formik.handleBlur}
            value={formik.values.lastName}
        />
        {formik.touched.lastName && formik.errors.lastName ? (
            <div>{formik.errors.lastName}</div>
        ) : null} <br />
        <label htmlFor="email">Email Address: </label>
        <input
            id="email"
            name="email"
            type="email"
            onChange={formik.handleChange}
            onBlur={formik.handleBlur}
            value={formik.values.email}
        />
      
        {/* {formik.errors.email ? <div>{formik.errors.email}</div> : null} */}
        {formik.touched.email && formik.errors.email ? (
            <div>{formik.errors.email}</div>
        ) : null} <br />
        <button type="submit">Submit</button>
    </form>
  );
};
export default SignupFormValidate;
yup的使用让我们看到了,功能不变的情况下代码可以更加的简洁。
Formik组件
以上我们所有的例子都是通过useFormik拿到我们控制formik表单的内容,但是这样不容易形成封装,也就是说我们无法进行实例传值。而Formik由于配备了React Context,因此Formik本身就可以管理所包裹的JSX
我们新建文件src\page\formik\com\basic-formik.tsx:
import React from 'react';
import { Formik } from 'formik';
import * as Yup from 'yup';
const SignupFormFormik = () => {
  return (
    <Formik
      initialValues={{ firstName: '', lastName: '', email: '' }}
      validationSchema={Yup.object({
        firstName: Yup.string()
          .max(15, 'Must be 15 characters or less')
          .required('Required'),
        lastName: Yup.string()
          .max(20, 'Must be 20 characters or less')
          .required('Required'),
        email: Yup.string().email('Invalid email address').required('Required'),
      })}
      onSubmit={(values, { setSubmitting }) => {
        setTimeout(() => {
          alert(JSON.stringify(values, null, 2));
          setSubmitting(false);
        }, 400);
      }}
    >
      {formik => (
        <form onSubmit={formik.handleSubmit}>
          <label htmlFor="firstName">First Name</label>
          <input
            id="firstName"
            type="text"
            {...formik.getFieldProps('firstName')}
          />
          {formik.touched.firstName && formik.errors.firstName ? (
            <div>{formik.errors.firstName}</div>
          ) : null} <br />
          
          <label htmlFor="lastName">Last Name</label>
          <input
            id="lastName"
            type="text"
            {...formik.getFieldProps('lastName')}
          />
          {formik.touched.lastName && formik.errors.lastName ? (
            <div>{formik.errors.lastName}</div>
          ) : null} <br />
          <label htmlFor="email">Email Address</label>
          <input id="email" type="email" {...formik.getFieldProps('email')} />
          {formik.touched.email && formik.errors.email ? (
            <div>{formik.errors.email}</div>
          ) : null} <br />
          <button type="submit">Submit</button>
        </form>
      )}
    </Formik>
  );
};
export default SignupFormFormik;
以上,我们可以用formik独有的Field来简写
import React from 'react';
import { Formik, Field, Form, ErrorMessage } from 'formik';
import * as Yup from 'yup';
const SignupFormFormikField = () => {
  return (
    <Formik
      initialValues={{ firstName: '', lastName: '', email: '' }}
      validationSchema={Yup.object({
        firstName: Yup.string()
          .max(15, 'Must be 15 characters or less')
          .required('Required'),
        lastName: Yup.string()
          .max(20, 'Must be 20 characters or less')
          .required('Required'),
        email: Yup.string().email('Invalid email address').required('Required'),
      })}
      onSubmit={(values, { setSubmitting }) => {
        setTimeout(() => {
          alert(JSON.stringify(values, null, 2));
          setSubmitting(false);
        }, 400);
      }}
    >
      <Form>
        <label htmlFor="firstName">First Name</label>
        <Field name="firstName" type="text" />
        <ErrorMessage name="firstName" /> <br />
        <label htmlFor="lastName">Last Name</label>
        <Field name="lastName" type="text" />
        <ErrorMessage name="lastName" /><br />
        <label htmlFor="email">Email Address</label>
        <Field name="email" type="email" />
        <ErrorMessage name="email" /><br />
        <button type="submit">Submit</button>
      </Form>
    </Formik>
  );
};
export default SignupFormFormikField;
所以我们在明白了forimik是如何运行之后,后续的封装也是基于Formik来处理,会单独讲解。
前面的例子的values值都只是一个普通对象,如果这个对象是深度的,那么该如何处理呢
array or object formik
Formik 开箱即用地支持嵌套对象和数组
Formik 中的props name可以使用类似 lodash 的点路径来引用嵌套的 Formik 值。这意味着您不再需要展平表单的值。
我们新建文件src\page\formik\com\basic-array-obj.tsx:
import React from 'react';
import { Formik, Field, Form, ErrorMessage } from 'formik';
import * as Yup from 'yup';
const BasicForArrayObj = () => {
  return (
    <Formik
      initialValues={
        { 
          firstName: '', 
          lastName: '', 
          email: '',
          social: {
            facebook: '',
            twitter: '',
          } 
        }
      }
      validationSchema={Yup.object({
        social: Yup.object({
          facebook: Yup.string()
          .max(15, 'Must be 15 characters or less')
          .required('Required'),
          twitter: Yup.string()
          .max(15, 'Must be 15 characters or less')
          .required('Required'),
        }),
        firstName: Yup.string()
          .max(15, 'Must be 15 characters or less')
          .required('Required'),
        lastName: Yup.string()
          .max(20, 'Must be 20 characters or less')
          .required('Required'),
        email: Yup.string().email('Invalid email address').required('Required'),
      })}
      onSubmit={(values, { setSubmitting }) => {
        setTimeout(() => {
          alert(JSON.stringify(values, null, 2));
          setSubmitting(false);
        }, 400);
      }}
    >
      <Form>
        <label htmlFor="twitter">facebook</label>
        <Field name="social.twitter" type="text" />
        <ErrorMessage name="social.twitter" /> <br />
        <label htmlFor="facebook">facebook</label>
        <Field name="social.facebook" type="text" />
        <ErrorMessage name="social.facebook" /> <br />
        <label htmlFor="firstName">First Name</label>
        <Field name="firstName" type="text" />
        <ErrorMessage name="firstName" /> <br />
        <label htmlFor="lastName">Last Name</label>
        <Field name="lastName" type="text" />
        <ErrorMessage name="lastName" /><br />
        <label htmlFor="email">Email Address</label>
        <Field name="email" type="email" />
        <ErrorMessage name="email" /><br />
        <button type="submit">Submit</button>
      </Form>
    </Formik>
  );
};
export default BasicForArrayObj;
注意:
- yup真丢对象校验使用Yup.object({})
 - 访问属性值通过点属性字符串
 
同理数组也是类似的做法,我们新建文件src\page\formik\com\basic-array-obj1.tsx:
import React from 'react';
import { Formik, Field, Form, ErrorMessage } from 'formik';
import * as Yup from 'yup';
const BasicForArrayObj2 = () => {
  return (
    <Formik
      initialValues={
        { 
          friends: ['', ''], 
        }
      }
      validationSchema={Yup.object({
        friends: Yup.array().of(Yup.string().required('Required')),
      })}
      onSubmit={(values, { setSubmitting }) => {
        setTimeout(() => {
          alert(JSON.stringify(values, null, 2));
          setSubmitting(false);
        }, 400);
      }}
    >
      <Form>
        <label htmlFor="friends[1]">friend2</label>
        <Field name="friends[1]" type="text" />
        <ErrorMessage name="friends[1]" /> <br />
        <label htmlFor="friends[0]">ftiend1</label>
        <Field name="friends[0]" type="text" />
        <ErrorMessage name="friends[0]" /> <br />
        <button type="submit">Submit</button>
      </Form>
    </Formik>
  );
};
export default BasicForArrayObj2;
注意:
- yup验证了每个数组内容,因此如果有一个没有值,将无法正常提交
 - yup验证数组:https://github.com/jquense/yup#arrayoftype-schema-this
 - 访问属性形式:friends[1]
 
常用api
ErrorMessage
捕捉错误的容器,相当于下列的简写,用法见上面的例子,必传一个name属性。
{formik.touched.lastName && formik.errors.lastName ? (
    <div>{formik.errors.lastName}</div>
) : null} <br />
Field
将自动将输入连接到 Formik,默认渲染是input输入框,他有以下几种方法渲染
直接渲染
<Field type="email" name="email" placeholder="Email" />
as转化成其他节点
<Field as="select" name="color">
  <option value="red">Red</option>
  <option value="green">Green</option>
  <option value="blue">Blue</option>
</Field>
渲染jsx
<Field name="lastName">
{({
  field, // { name, value, onChange, onBlur }
  form: { touched, errors }, // also values, setXXXX, handleXXXX, dirty, isValid, status, etc.
  meta,
}) => (
  <div>
    <input type="text" placeholder="Email" {...field} />
    {meta.touched && meta.error && (
      <div className="error">{meta.error}</div>
    )}
  </div>
)}
</Field>
以component组件形式渲染
<Field name="lastName" placeholder="Doe" component={MyInput} />
getIn()
获取点属性的值,在values包含数组或者深层次对象的时候有用,可以对比上述案例。
setValues()和setFieldValue()
setValues()设置formik的values属性值,setFieldValue()设置values的某一个属性值,二者写法不同,但是目的一样:改变values的值。使用他们之前最好配套useFormik使用:
formik.setFieldValue('firstName','zhangsan');
formik.setValues({
    email: '',
    firstName: 'lisi',
    lastName: '',
})
案例
主要讲解了如何封装formik,初始化formik,formik和Mui的使用,以及表单提交
来一个简单的
首先建立一个form.tsx ,他会接收一个formik实例,本质上用到了React.Provider,确保了在其包裹内可以使用useFormik进行获取对应的values值。
import type { FormikConfig,FormikValues,FormikContextType } from 'formik';
import type { ReactNode } from 'react';
import { FormikProvider,Form, } from 'formik';
import React from 'react';
interface FormProps<FormikValues> {
    instance: FormikContextType<FormikValues>,
    children: ReactNode
}
const ZLForm = <FormikValues extends object>(props: FormProps<FormikValues>) => {
    const { instance,children } = props;
    return (
        <FormikProvider value={instance}>
            <Form>
                {children}
            </Form>
        </FormikProvider>
    )
}
export default ZLForm;
然后建立一个功能组件wrapper.tsx
初始化formik
 const testFormik = useFormik<Person>({
    initialValues: {
      username: 'jack',
      age: 12
    },
    onSubmit:(values) => {
      console.log('i am submit',values)
    }
  })
获取values
const { values,setFieldValue } = testFormik;
设置初始值,初始值如果不使用的话,便会使用默认的initialValues
  useEffect(() => {
    setFieldValue('username',initialValue.username);
    setFieldValue('age',initialValue.age);
  },[])
主要业务结构,这里需要注意使用受控组件,即表单的value值和他的onChange事件要对上来;当我们input框发生改变的时候,会触发testFormik.handleChange;type='submit'相当于表单form的submit,会触发formik的onSubmit事件
另外ZLInput是封装的组件,见gitee:https://gitee.com/xifeng-canyang/jest-formik/blob/master/src/component/common/Input.tsx
  <ZLForm instance={testFormik}>
    <Grid container>
      <Grid item xs={12}>
        <ZlInput name='username' label='username' value={values.username} handleInputChange={testFormik.handleChange} />
        <ZlInput name='age' label='age' value={values.age} handleInputChange={testFormik.handleChange} />
      </Grid>
      <Grid item xs={12}>
        <Button variant="contained" type='submit'>submit</Button>
      </Grid>
    </Grid> 
  </ZLForm>

完整的demo:https://gitee.com/xifeng-canyang/jest-formik/blob/master/src/page/formik/advance/wrapper.tsx
再来一个难一点的
难一点是指,我们的初始值不在是一个简单的对象,而是对象、数组、深层次对象和数组对象的结合体。本例子可以在gitee上找到:https://gitee.com/xifeng-canyang/jest-formik/blob/master/src/page/formik/advance/wrapper-complex.tsx
注意点:
- gitee代码是都写在了一个文件里面,因此显得业务比较臃肿,实际开发建议抽出公共组件,从而降低代码的复杂度
 - 对于深层次的数据展示我们是使用点运算符
 - 对于深层次数据的获取则是使用getIn
 
我们新建一个文件wrapper-complex
初始值示例
const testFormik = useFormik<Person>({
  initialValues: {
    username: 'jack',
    age: 12,
    like: {
      sport: 'yes',
      pingPong: 'no'
    },
    subject: ['math','english','chinese'],
    subjectDetail: [
      {
        subject: 'math',
        score: 80
      },
      {
        subject: 'english',
        score: 76
      },
      {
        subject: 'chinese',
        score: 95
      }
    ]
  },
  onSubmit:(values) => {
    console.log('i am submit',values)
  }
})
简单的对象展示
<Grid item xs={12}>
    <ZlInput name='username' label='username' value={values.username} handleInputChange={testFormik.handleChange} />
    <ZlInput name='age' label='age' value={values.age} handleInputChange={testFormik.handleChange} />
</Grid>
深层次的对象展示
<Grid item xs={12}>
{
  Object.keys(values.like).map(subkey => {
    return (
        <ZlInput key={subkey} name={`like.${subkey}`} label={getIn(values,`like.${subkey}`)} value={getIn(values,`like.${subkey}`)} handleInputChange={testFormik.handleChange} />
    )
  })
}
</Grid>
数组展示
<Grid item xs={12}>
{
  values.subject.map((item,index) => {
    return (
      <ZlInput key={item} name={`subject[${index}]`} label={getIn(values,`subject[${index}]`)} value={getIn(values,`subject[${index}]`)} handleInputChange={testFormik.handleChange} />
    )
  })
}
</Grid>
数组对象结合展示
<Grid container>
{
  values.subjectDetail.map((item,index) => {
    const keyArr = Object.keys(item);
    const uniKey = JSON.stringify(item);
    return (
      <Title key={uniKey}>
        <Title>科目:{item.subject}</Title>
        <ZlInput name={`subjectDetail[${index}].${keyArr[1]}`} label={getIn(values,`subjectDetail[${index}].${keyArr[1]}`)} value={getIn(values,`subjectDetail[${index}].${keyArr[1]}`)} handleInputChange={testFormik.handleChange} />
      </Title>
    )
  })
}
</Grid>
效果图

看到这里也不容易,帮忙点点赞支持支持~