表单校验关于前端来说无疑是个繁琐的事情,好在后台办理组件库例如Antd、Element帮咱们处理了这些麻烦。可是关于C端说,UI设计师总会为网站定制特性的表单页面,这不得不需求咱们切图搞起来。既然结构层和表明层需求咱们亲主动手,而表单校验的行为层逻辑总是相似的,有没有一款好用又便利、又接地气又高逼格的库让咱们解放双手呢?唉,还真有,那便是今日引荐的React-Hook-Form,Github上Star有30多k。看它的官方文档真是享受,因为它让我感觉本来组件还能这样封装,逻辑还能这样处理,格式打开了。有种醍醐灌顶的感觉,妈妈,本来我又会了。
根本用法
咱们来看下它的根本用法
import { useForm } from "react-hook-form";
function Demo() {
const { register, handleSubmit, formState: { errors } } = useForm();
const onSubmit = handleSubmit((data) => console.log(data))
return (
<div className="Demo">
<form onSubmit={onSubmit}>
<input {...register("name")} />
<input {...register("desc", { required: "请输入描绘" })} />
{errors.desc && <span>{errors.desc.message}</span>}
<input type="submit" />
</form>
</div>
);
}
export default Demo;
十分便利,只需求将运用register('name')
注册一个name的表单项,并将回来值注入到input框中。假如input框是必填项,register办法供给第二个参数,register("desc", { required: "请输入描绘" })
。咱们能够从formState.errors
字段获取校验的错误信息进行显现。接着handleSubmit
是一个高阶函数,可传入回调函数。handleSubmit(data => console.log(data))
回来onSubmit
函数,当咱们触发onSubmit
时可在回调函数中获取表单数据。
register还能够自定义校验规则,例如
register("name", {
validate: (value) => {
if (/^[A-Za-z]+$/i.test(value)) {
return true;
}
return "只允许输入英文字母";
},
})
当然,一些常用的min
、max
、minLength
等校验特点都是能够直接运用。
你可能会猎奇<input {...register("name")} />
中register究竟回来了哪些字段给到表单项,咱们应该大约猜到无非便是onChange
、value
之类的特点,啀,还不彻底定对。它回来的主要字段有name
、onChange
、onBlur
、ref
,没有value
?没有!React Hook Form
注重削减重烘托以达到高性能的目的,采用非受控组件的办法。经过ref拿到input
、select
、textarea
等原生组件节点来设置value值。
因为不需求特意运用useState
来实时存储表单数据,因此咱们输入框输入等操作时,并不会影响组件从头烘托。
formState监听表单状况
useForm
还回来了formState
字段,里边有校验错误信息、是否在校验、是否提交等等特点。
const {
formState: {
errors, isDirty, isValidating, isSubmitted
// ...
},
} = useForm();
这些特点被开发者运用且改动时,才会触发组件烘托,不运用时不会形成重烘托。什么意思呢?咱们运用errors
字段来看下区别。
没有运用errors字段,不会触发重烘托
运用errors字段,当errors变化时会主动触发重烘托,获取最新的errors数据
只需求根据咱们是否解构运用来判别,是否需求监听errors变化。第一眼看上去是不是很神奇,很有灵性,不需求开发者操心,就能够防止一些不必要的性能耗费。那它是怎样做到的呢?细心想想咱们怎样监听是否运用了某个字段,当然便是咱们陈词滥调的Object.defineProperty
或者Proxy
。还真是,源码传送门
大约的思路便是
const initialFormState = {
isDirty: false,
isValidating: false,
errors: {},
// ...
}
function useForm() {
const [formState, updateFormState] = useState({ ...initialFormState })
const _formControl = React.useRef({
control: {
_formState: { ...initialFormState },
_proxyFormState: {}
}
});
// ...
// 对formState进行代理
_formControl.current.formState = getProxyFormState(
formState,
_formControl.current.control
);
return _formControl.current;
}
function getProxyFormState(formState, control) {
const result = {};
for (const key in formState) {
Object.defineProperty(result, key, {
get: () => {
// 只需开发者解构运用了某个字段,即触发了get办法,则设置该字段代理状况为true
control._proxyFormState[key] = true;
return formState[key];
},
});
}
return result
}
useForm
只用了一个useState
,一般不会去更新state
的状况,而是用useRef
创建的_formControl.control._formState
来存储最新值,这样确保不会触发组件更新。
例如errors
字段有变动了,才会更新useState的值
// errors有变化时,且_proxyFormState.errors === true
if (_formControl.control._proxyFormState.errors) {
// 更新useState中的值,触发重烘托
updateFormState({ ...control._formState });
}
register回来的ref
处理了咱们的猎奇,接着往下讲。前面提到register("name")
回来的一些字段name
、onChange
、onBlur
、ref
等会挂载到表单组件上,那假如咱们本身需求拿到表单组件的ref,或者监听事情怎样办?
function Demo() {
const inputRef = useRef(null);
const { register, handleSubmit } = useForm();
const { ref, onBlur, ...rest } = register("name", {
required: "请输入称号",
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input
onBlur={(e) => {
onBlur(e);
// 处理blur事情
console.log("blur");
}}
ref={(e) => {
ref(e);
// 拿到输入框ref
inputRef.current = e;
}}
{...rest}
/>
<input type="submit" />
</form>
);
}
那你要说register
上述办法不得劲啊,有时候自己封装的表单组件没有供给ref,或者便是不想经过ref来绑定。那也是能够手动setValue
。
const CustomInput = ({ name, label, value, onChange }) => (
<>
<label htmlFor={name}>{label}</label>
<input name={name} value={value} onChange={onChange} />
</>
);
function Demo() {
const {
register,
handleSubmit,
setValue,
setError,
watch,
formState: { errors },
} = useForm({ defaultValues: { name: '' } });
const onValidateName = (value) => {
if (!value) {
setError("name", { message: "请输入" });
} else if (!/^[A-Z]/.test(value)) {
setError("name", { message: "首字符有必要为大写字母" });
}
};
useEffect(() => {
register("name");
}, []);
return (
<form
onSubmit={handleSubmit((data) => {
// 手动增加触发onSubmit时校验
if (!onValidateName(data.name)) {
return;
}
console.log(`提交:${JSON.stringify(data)}`);
})}
>
<CustomInput
label="称号"
name="name"
value={watch("name")}
onChange={(value) => {
// 手动增加触发onChange时校验
onValidateName(value);
setValue("name", value);
}}
/>
{errors.name && <span>{errors.name.message}</span>}
<input type="submit" />
</form>
);
}
能够看到,假如咱们需求受控组件的办法能够运用value={watch("name")}
传递给组件(当然纷歧定需求)。
假如需求输入操作时能够触发校验规则,只能够手动增加了,上面咱们封装了onValidateName
校验函数,为了让输入改动和提交表单时校验规则一致,所以在onChange
和handleSubmit
回调函数中都增加了校验。
看起来是麻烦了点,假如一定要受控组件并且纷歧定能供给ref,这个库也为咱们考虑了这种情况,供给了Controller
组件给咱们,这样就简洁一点了。
import { Controller, useForm } from "react-hook-form";
const CustomInput = ({ name, label, value, onChange }) => (
<>
<label htmlFor={name}>{label}</label>
<input name="name" value={value} onChange={onChange} />
</>
);
function Demo() {
const {
control,
handleSubmit,
formState: { errors },
} = useForm({
defaultValues: {
name: "",
},
});
return (
<form
onSubmit={handleSubmit((data) => {
console.log(`提交:${JSON.stringify(data)}`);
})}
>
<Controller
render={({ field }) => <CustomInput label="称号" {...field} />}
name="name"
control={control}
rules={{
required: {
value: true,
message: "请输入",
},
pattern: {
value: /^[A-Z]/,
message: "首字符有必要为大写字母",
},
}}
/>
{errors.name && <span>{errors.name.message}</span>}
<input type="submit" />
</form>
);
}
仍是回归到最初,假如咱们能供给ref,仍是尽量供给吧。非受控组件至少能削减烘托次数。例如运用forwardRef
const CustomInput = forwardRef(({ name, label, onChange, onBlur }, ref) => (
<>
<label htmlFor={name}>{label}</label>
<input name={name} onChange={onChange} onBlur={onBlur} ref={ref} />
</>
));
表单联动
前面提到React-hook-form
运用的是非受控组件计划,那假如咱们需求实时获取监听最新的表单值呢?能够如下
function Demo() {
const { watch } = useForm();
// 监听单个值
const name = watch('name');
// 监听多个值
const [name2, desc] = watch(['name', 'desc'])
// 监听所有值
useEffect(() => {
const { unsubscribe } = watch((values) => {
console.log("values", values);
});
return () => unsubscribe();
}, []);
return (
<form>
<input {...register("name")} />
<input {...register("desc", { required: "请输入描绘" })} />
</form>
);
}
这儿运用了观察者模式,只需咱们对需求观察的字段值改动了,才会触发组件烘托。
那么运用watch
,咱们能够很简略做到表单联动
export default function Demo() {
const { register, watch, handleSubmit } = useForm({
shouldUnregister: true,
});
const [data, setData] = useState({});
return (
<div className="App">
<form onSubmit={handleSubmit(setData)}>
<div>
<label htmlFor="name">称号:</label>
<input {...register("name")} />
</div>
<div>
<label htmlFor="more">更多:</label>
<input type="checkbox" {...register("more")} />
</div>
{watch("more") && (
<div>
<label>年龄:</label>
<input type="number" {...register("age")} />
</div>
)}
<input type="submit" />
</form>
<div>提交数据:{JSON.stringify(data)}</div>
</div>
);
}
是不是很便利,传入useForm({ shouldUnregister: true });
,就能够主动撤销注册不需求的表单项,比如上面的年龄。Reat-hook-form
又是咋做到主动撤销注册不要的表单项呢,仍是从ref上。
首要询问下<input ref={e => console.log(e)} />
一般从挂载到注销会打印几回?一般两次,第一次打印input的dom节点,另一次打印null
。
所以React-hook-form
也是判别null
时撤销注册的,下面也描绘下简略做法
const _names = {
unMount: new Set()
}
ref(ref) {
if (ref) {
register(name, options);
} else {
options.shouldUnregister && unMount.add(name);
}
}
然后在useEffect中撤销注册
useEffect({
const _removeUnmounted = () => {
for (const name of _names.unMount) {
unregister(name)
}
_names.unMount = new Set();
}
_removeUnmounted()
})
咱们现在了解了根本原理,那前面说不供给ref的组件咋么办,shouldUnregister
是不会起作用,那只能手动移除了
export default function Demo() {
const { register, watch, handleSubmit, unregister, setValue } = useForm();
const [data, setData] = useState({});
const more = watch("more");
useEffect(() => {
if (more) {
register("age");
} else {
unregister("age");
}
}, [more]);
return (
<div className="App">
<form onSubmit={handleSubmit(setData)}>
<div>
<label htmlFor="name">称号:</label>
<input {...register("name")} />
</div>
<div>
<label htmlFor="more">更多:</label>
<input type="checkbox" {...register("more")} />
</div>
{more && (
<CustomInput
label="年龄"
name="age"
onChange={(value) => setValue("age", value)}
/>
)}
<input type="submit" />
</form>
<div>提交数据:{JSON.stringify(data)}</div>
</div>
);
}
或者运用上面说的库供给的Controller
组件
基于React-hook-form封装表单组件
最终,假定咱们开发好了咱们的表单组件,再结合React-hook-form
校验库运用,就能够完结咱们网站专属的表单页啦。假如咱们的表单组件在网站或项目中多个地方用到,或许咱们能够再进一层封装。如下运用是不是简介很多。
function Demo() {
const [data, setData] = useState({});
return (
<div className="App">
<Form onFinish={setData}>
<FormItem label="称号" name="name" rule={{ required: "请输入称号" }}>
<CustomInput />
</FormItem>
<FormItem label="性别" name="gender" rule={{ required: "请挑选性别" }}>
<CustomSelect options={["男", "女", "其他"]} />
</FormItem>
</Form>
<div>提交数据:{JSON.stringify(data)}</div>
</div>
);
}
咱们现在来动手简略实现一个。首要是咱们自定义开发的表单组件,例如输入框、挑选框等。
const CustomInput = React.forwardRef(({ size = "middle", ...rest }, ref) => (
<input {...rest} className={`my-input my-input-${size}`} ref={ref} />
));
const CustomSelect = React.forwardRef(
({ size = "middle", options, placeholder = "请挑选", ...rest }, ref) => (
<select {...rest} className={`my-select my-select-${size}`} ref={ref}>
<option value="">{placeholder}</option>
{options.map((value) => (
<option key={value} value={value}>
{value}
</option>
))}
</select>
)
);
紧接着咱们封装Form
容器组件
const Form = ({ children, defaultValues, onFinish }) => {
const {
handleSubmit,
register,
formState: { errors },
} = useForm({ defaultValues });
return (
<form onSubmit={handleSubmit(onFinish)}>
{React.Children.map(children, (child) =>
child.props.name
? React.cloneElement(child, {
...child.props,
register,
error: errors[child.props.name],
key: child.props.name,
})
: child
)}
<input type="submit" />
</form>
);
};
一般Form中的children便是FormItem组件,咱们对其props弥补了register办法和error。
然后咱们再来封装下FormItem组件
const FormItem = ({ children, name, label, register, rule, error }) => {
// 简略处理:判别FormItem 只能传入一个child
const child = React.Children.only(children);
return (
<div>
<label htmlFor={name}>{label}</label>
{React.cloneElement(child, {
...child.props,
...register(name, rule),
name,
})}
{error && <span>{error.message}</span>}
</div>
);
};
FormItem组件的children一般便是输入框、挑选框等,咱们调用register办法将回来的ref、onChange等特点再弥补到输入框、挑选框等表单组件上。
至此,咱们自行封装的表单组件库demo版就完结啦。那其实咱们还有很多容错判别、更多功能还没有处理,能够渐渐增加。例如咱们需求有重置表单的功能
function Demo() {
const [data, setData] = useState({});
const formRef = useRef();
return (
<div className="App">
<Form onFinish={setData} ref={formRef}>
<FormItem label="称号" name="name" rule={{ required: "请输入称号" }}>
<CustomInput />
</FormItem>
</Form>
<div onClick={() => formRef.current.reset()}>
重置
</div>
</div>
);
}
那么咱们的Form组件就需求把useForm回来的办法等暴露出去
const Form = React.forwardRef(({ children, defaultValues, onFinish }, ref) => {
const form = useForm({ defaultValues });
const {
handleSubmit,
register,
formState: { errors },
} = form;
React.useImperativeHandle(ref, () => form);
return (
<form onSubmit={handleSubmit(onFinish)}>
{React.Children.map(children, (child) =>
child.props.name
? React.cloneElement(child, {
...child.props,
register,
error: errors[child.props.name],
key: child.props.name,
})
: child
)}
<input type="submit" />
</form>
);
});
好啦太多需求弥补了,就纷歧一讲述。
最终
经过学习运用React-hook-form,给开发节约不少时刻,也get到了不少技巧,收获满满的,也期望对你有用。