Formik 优化表单操作
一、Formik 的定义
React 操作 Form 表单不是很友好,当访问表单中 input 控件的值时,通常有两种操作:
第一种情况可能需要操作 DOM,官方不推荐使用;而第二种情况为受控组件,当要改动控件数据时,需要再绑定一个 onChange
事件,当控件过多时操作会很繁琐。而 Formik 的出现,将表单操作化繁为简,使表单操作变简单。
1、什么是 Formik
Formik 库提供了针对表单的状态管理,还能很方便的对表单做规则校验和处理表单提交。
2、Formik 的原理
Formik 内置了表单的 state 管理操作,无需单独为表单建立 state,同时使用了 Context,让表单组件可以多层嵌套,避免了层层传递的操作。
3、与 Redux-Form 的区别
表单状态本质上是短暂且局部的,所以没必要在 Redux 中跟踪它。而且每触发一个键(ON EVERY SINGLE KEYSTROKE),Redux-Form 都会多次调用整个顶级 Redux Reducer,随着项目的增长,这将带来输入延迟的问题。
二、Formik 的使用
1、Formik 库的安装
# npm
npm install formik --save
# yarn
yarn add formik
2、useFormik 创建表单内容
以上分别是 type 为 text、password 和 submit 的三个 input 控件,对于这种多控件的表单,可以使用 useFormik 创建管理表单内容,useFormik
接收一个配置对象,创建的内容用于表单绑定,包含:
- values:表单数据;
- handleChange:表单项的 onChange 事件处理函数,用于数据同步;
- handleSubmit:表单提交事件处理函数。
每个表单项通过 name、value、onChange 与 formik 创建的内容进行绑定,使用如下:
import React from 'react'
import { useFormik } from 'formik'
const App = () => {
const formik = useFormik({
// 表单默认数据
initialValues: {
username: '',
password: ''
},
// 表单提交事件处理函数,它接收表单数据作为参数
// 默认 Formik 默认帮我们阻止了表单默认行为,不需要重复编写
onSubmit: (values) => {
console.log(values)
}
})
return (
<form onSubmit={formik.handleSubmit}>
<div>
<input
type="text"
name="username"
value={formik.values.username}
placeholder="请输入用户名"
onChange={formik.handleChange}
/>
</div>
<div>
<input
type="password"
name="password"
value={formik.values.password}
placeholder="请输入密码"
onChange={formik.handleChange}
/>
</div>
<input type="submit" />
</form>
)
}
export default App
点击提交按钮,输出以下结果:
- 另外,可以用解构 useFormik 的方式替代 formik 声明,简化每次都要
formik.xx
的代码; - 对于一些无法使用
handleChange
链接更新 formik 表单的组件,可以使用 setFieldValue 单独设置,例如:
<Select
options={options}
value={values.selectVal}
onChange={(val) => {
setFieldValue('selectVal', val)
}}
/>
3、validate 项配置表单验证
useFormik
可以配置表单验证方式 validate。validate
接收表单数据作为参数,一般返回一个 errors 对象作为验证结果。使用如下:
import React from 'react'
import { useFormik } from 'formik'
const App = () => {
const formik = useFormik({
initialValues: {
username: '',
password: ''
},
validate: (values) => {
const errors = {}
const { username, password } = values
// 用户名验证
if (!username) {
errors.username = '请输入用户名'
} else if (username.length > 15) {
errors.username = '用户名长度不得超过15个字'
}
// 密码验证
if (!password) {
errors.password = '请输入密码'
} else if (password.length < 6) {
errors.password = '密码长度不得小于6'
}
return errors
},
onSubmit: (values) => {
console.log(values)
}
})
return (
<form onSubmit={formik.handleSubmit}>
<div>
<input
type="text"
name="username"
value={formik.values.username}
placeholder="请输入用户名"
onChange={formik.handleChange}
/>
{formik.errors.username && <span>{formik.errors.username}</span>}
</div>
<div>
<input
type="password"
name="password"
value={formik.values.password}
placeholder="请输入密码"
onChange={formik.handleChange}
/>
{formik.errors.password && <span>{formik.errors.password}</span>}
</div>
<input type="submit" />
</form>
)
}
export default App
验证效果如下:
可以看到,输入用户名时自动对下面的密码项进行了校验,而且在输入时进行了实时验证,需要进行以下优化:
- 开启失去焦点时再触发验证
useFormik
返回的对象包含 handleBlur 方法,将其绑定到表单项的 onBlur 属性上可以在失去焦点时才触发验证。
- 开启初始数据未变更的表单项不进行校验
useFormik
返回对象中包含 touched 属性,存储了表单发生变化的数据;- 只有当表单项绑定了 handleBlur 后,touched 才会记录。
具体优化如下:
// ...
return (
<form onSubmit={formik.handleSubmit}>
<div>
<input
type="text"
name="username"
value={formik.values.username}
placeholder="请输入用户名"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
{formik.errors.username && (
{(formik.touched.username && formik.errors.username) && (
<span>{formik.errors.username}</span>
)}
</div>
<div>
<input
type="password"
name="password"
value={formik.values.password}
placeholder="请输入密码"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
{formik.errors.password && (
{(formik.touched.password && formik.errors.password) && (
<span>{formik.errors.password}</span>
)}
</div>
<input type="submit" />
</form>
)
优化后效果如下:
4、Yup 校验库优化表单验证写法
Yup 是一个 JavaScript 方案生成器,用于值的解析转化和验证。
4-1、安装
# npm
npm install yup --save
# yarn
yarn add yup
4-2、基本用法
useFormik
的配置选项中通过 validationSchema 来配置验证规则,object()
用于创建验证规则,传入一个对象,key 是表单项的 name,值是 yup
的验证方法:
import React from 'react'
import { useFormik } from 'formik'
import * as Yup from 'yup'
const App = () => {
const formik = useFormik({
initialValues: {
username: '',
password: ''
},
validate: (values) => {
const errors = {}
const { username, password } = values
if (!username) {
errors.username = '请输入用户名'
} else if (username.length > 15) {
errors.username = '用户名长度不得超过15个字'
}
if (!password) {
errors.password = '请输入密码'
} else if (password.length < 6) {
errors.password = '密码长度不得小于6'
}
return errors
},
validationSchema: Yup.object({
username: Yup.string()
.max(15, '用户名长度不得超过15个字')
.required('请输入用户名'),
password: Yup.string()
.min(6, '密码长度不得小于6')
.required('请输入密码')
}),
onSubmit: (values) => {
console.log(values)
}
})
return (
<form onSubmit={formik.handleSubmit}>
<div>
<input
type="text"
name="username"
value={formik.values.username}
placeholder="请输入用户名"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
{(formik.touched.username && formik.errors.username) && (
<span>{formik.errors.username}</span>
)}
</div>
<div>
<input
type="password"
name="password"
value={formik.values.password}
placeholder="请输入密码"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
{(formik.touched.password && formik.errors.password) && (
<span>{formik.errors.password}</span>
)}
</div>
<input type="submit" />
</form>
)
}
export default App
4-3、优化验证
- 可通过
formik.isValid
判断表单是否验证通过; - 可通过
formik.errors
查看当前表单哪些未通过验证; - 可通过 validateOnMount 设置加载后立即验证,例如:
const formik = useFormik({
initialValues: {
username: '',
password: ''
},
validateOnMount: true,
// ...
})
5、getFieldProps 减少样板代码
useFormik
返回的对象提供了一个 getFieldProps 方法,用于获取指定表单项的 name、value、onChange、onBlur 属性,可以将它们直接绑定到表单项元素,减少重复代码。使用如下:
import React from 'react'
import { useFormik } from 'formik'
import * as Yup from 'yup'
const App = () => {
const formik = useFormik({
initialValues: {
username: '',
password: ''
},
validationSchema: Yup.object({
username: Yup.string() // 将值(不为空时)转化为字符串
.max(15, '用户名长度不得超过15个字') // 最大长度
.required('请输入用户名'), // 必传
password: Yup.string().min(6, '密码长度不得小于6').required('请输入密码')
}),
onSubmit: (values) => {
console.log(values)
}
})
return (
<form onSubmit={formik.handleSubmit}>
<div>
<input
type="text"
placeholder="请输入用户名"
name="username"
value={formik.values.username}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
{...formik.getFieldProps('username')}
/>
{formik.touched.username && formik.errors.username && (
<span>{formik.errors.username}</span>
)}
</div>
<div>
<input
type="text"
placeholder="请输入密码"
name="password"
value={formik.values.password}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
{...formik.getFieldProps('password')}
/>
{formik.touched.password && formik.errors.password && (
<span>{formik.errors.password}</span>
)}
</div>
<input type="submit" />
</form>
)
}
export default App
6、用 Formik 提供的组件构建表单
6-1、<Formik />
<Formik />
为单外层组件,用来包裹 Form 组件,通过属性传递配置项,例如:
- initialValues(表单默认数据)
- onSubmit(表单提交事件处理函数)
- validationSchema(表单配置的验证规则)
6-2、<Form />
<Form />
为表单组件,在内部包裹一个个具体的表单项。
6-3、<Field />
<Field />
为表单项组件,name
属性绑定目标表单项的 name
,默认 Field 渲染为一个文本框(text 类型),可通过 as 属性指定类型,例如:
<Field name="content" as="textarea" />
<Field name="content" as="select" />
注意:Field 组件并没有提供现成的 password、单选框、复选框这样的表单控件,可以通过 useField
方法自定义控件。
6-4、<ErrorMessage />
<ErrorMessage />
用于显示表单验证失败的提示信息,name
属性绑定目标表单项的 name
。
6-5、使用示例
代码改造过程如下:
import React from 'react'
import { useFormik } from 'formik'
import { Formik, Form, Field, ErrorMessage } from 'formik'
import * as Yup from 'yup'
const App = () => {
const formik = useFormik({
initialValues: {
username: '',
password: ''
},
validationSchema: Yup.object({
username: Yup.string() // 将值(不为空时)转化为字符串
.max(15, '用户名长度不得超过15个字') // 最大长度
.required('请输入用户名'), // 必传
password: Yup.string().min(6, '密码长度不得小于6').required('请输入密码')
}),
onSubmit: (values) => {
console.log(values)
}
})
const initialValues = {
username: '',
password: ''
}
const validationSchema = Yup.object({
username: Yup.string()
.max(15, '用户名长度不得超过15个字')
.required('请输入用户名'),
password: Yup.string()
.min(6, '密码长度不得小于6')
.required('请输入密码')
})
const onSubmit = (values) => {
console.log(values)
}
return (
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={onSubmit}
>
<Form>
<form onSubmit={formik.handleSubmit}>
<div>
<input
type="text"
placeholder="请输入用户名"
{...formik.getFieldProps('username')}
/>
<Field name="username" placeholder="请输入用户名" />
{formik.touched.username && formik.errors.username && (
<span>{formik.errors.username}</span>
)}
<span>
<ErrorMessage name="username" />
</span>
</div>
<div>
<input
type="text"
placeholder="请输入密码"
{...formik.getFieldProps('password')}
/>
<Field name="password" placeholder="请输入密码" />
{formik.touched.password && formik.errors.password && (
<span>{formik.errors.password}</span>
)}
<span>
<ErrorMessage name="password" />
</span>
</div>
<input type="submit" />
</form>
</Form>
</Formik>
)
}
export default App
改造后代码如下:
import React from 'react'
import { Formik, Form, Field, ErrorMessage } from 'formik'
import * as Yup from 'yup'
const App = () => {
const initialValues = {
username: '',
password: ''
}
const validationSchema = Yup.object({
username: Yup.string()
.max(15, '用户名长度不得超过15个字')
.required('请输入用户名'),
password: Yup.string()
.min(6, '密码长度不得小于6')
.required('请输入密码')
})
const onSubmit = (values) => {
console.log(values)
}
return (
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={onSubmit}
>
<Form>
<div>
<Field name="username" placeholder="请输入用户名" />
<span>
<ErrorMessage name="username" />
</span>
</div>
<div>
<Field name="password" placeholder="请输入密码" />
<span>
<ErrorMessage name="password" />
</span>
</div>
<input type="submit" />
</Form>
</Formik>
)
}
export default App
7、useField 构建自定义表单控件
上面提到的 <Field />
组件没有提供现成的 password、单选框、复选框这样的表单控件,可以通过 useField
方法构建自定义表单控件。
useField` 可以获取表单项信息,返回一个数组,包含两项内容:
field
:包含表单属性相关的内容,name、value、onChange、onBlur 等;meta
:包含表单验证相关的信息。
const MyInputField = ({ label, ...props }) => {
const [field, meta] = useField(props)
return (
<div>
<label htmlFor={props.id}>{label}</label>
<input {...field} {...props} />
<span>{meta.touched && meta.error ? meta.error : null}</span>
</div>
)
}
7-1、自定义 checkbox 控件
import React from 'react'
import { Formik, Form, useField } from 'formik'
function MyCheckBox({ label, ...props }) {
const [field, meta, helper] = useField(props)
const { value } = meta
const { setValue } = helper
const handleChange = () => {
const set = new Set(value)
if (set.has(props.value)) {
set.delete(props.value)
} else {
set.add(props.value)
}
setValue([...set])
}
return (
<label htmlFor="">
<input
checked={value.includes(props.value)}
type="checkbox"
{...props}
onChange={handleChange}
/>
{label}
</label>
)
}
const App = () => {
const initialValues = {
hobbies: ['足球']
}
const onSubmit = (values) => {
console.log(values)
}
return (
<Formik initialValues={initialValues} onSubmit={onSubmit}>
<Form>
<div>
<MyCheckBox value="足球" label="足球" name="hobbies" />
<MyCheckBox value="篮球" label="篮球" name="hobbies" />
</div>
<input type="submit" />
</Form>
</Formik>
)
}
export default App
全选后点击提交,控制台输出: