前言
- 有小伙伴说写组件库封装表单没有思路。我一开始感觉确实有点不太好写,如果自己写了自己用也就算了,做成封装给别人用的那考虑东西就有点多。后来有人发了个react-hook-form的仓库,我一看,这种利用自定义hook做验证的思路很不错啊,于是自己手写个。
组件库线上体验地址
原理
- react-hook-form的使用方式需要拿到组件实例,对于我的组件库基本都是用函数组件写的且并未用forwardRef包装的轻量级组件库是不起作用的,除非我放弃原来封装好的组件,重新写,那肯定不划算。所以自己写个类react-hook-form且支持受控组件就是这次目标。
- 我并没有照抄react-hook-form的形式,毕竟那个库好几个文件,每个文件都接近千行,我都懒得看,但我从它的使用方式大致推测了干了啥事。而我操作受控组件也是这样做就可以了。
- 对于受控组件,也就是状态在组件内进行维护,一般暴露给外界的接口就是onchange接口,当用户输入或者点击时,onchange会被触发,我的组件库封装的组件暴露的onchange接口改了个名,叫callback。
难点1:如何把不知道数量的受控组件的回调转移到自定义hook中?
- 这个感觉是最大难点,解决了这个问题,基本上就出来了。
- 这个难点可以拆成2个小问题,一个是组件数量不知道,一个是受控组件回调转移。
- react-hook-form上直接拿实例,就不存在回调转移问题,但有组件数量不知道问题,所以react-hook-form让用户自己调用其方法去register组件。我也可以利用这种思路解决这个问题,就是让用户自己调。
- 解决数量问题,剩下就是回调问题,这个问题让我想了挺久的,后来想到这么个思路:自定义hook是可以接收参数的,那么我让用户把参数填好,吐出个对象,用户再用这个对象上的方法去回调受控组件的回调,这样就解决了。当然,组件数量不同,用的方法不同,所以让用户在输入参数的时候制定name,吐出来的对象里使用对应的name去回调就完成了。
难点2:回调传入受控组件如何收集状态?
- 这个问题其实挺好解决的,在自定义hook内部建立状态,当用户输入触发回调,就会过一遍自定义hook,将数据setState就ok了。
难点3:如何传入多个验证器,并且反馈对应的验证结果?
- 这个主要是数据格式问题,我一开始没理清反馈格式,后来发现验证器应该可以传多个,并且每个验证器会有对应的提示信息。
- 正常来说,使用组件的人不需要关心哪个验证器通过哪个验证器没通过,只要把你返回的验证结果输出就可以了。所以对于返回的验证结果,使用数组字符串形式解决。感觉any真是TS最伟大发明,先全any,保证能使用再说,然后再慢慢精确格式。
- 在触发回调时候,同时让验证器走一遍,拿到结果,设置到状态里,吐出状态给使用的组件。使用的组件就拿到验证结果,最后使用结果进行相应渲染。
难点4:如何解决dirty脏数据验证?
- 这个也好解决,根据难点1的方式,吐出个对象,让用户传给onBlur触发Blur,函数里存入状态并进行判定,如果blur状态里没这个组件,说明没dirty,加上状态,如果有这个组件,说明dirty了。
- 验证逻辑就加在第一次dirty处即可。 后续不加验证。
代码:
interface ValidateType{
validate:(e:any)=>boolean
message:string;
}
interface UseFormProps{
name:string;
validate?:Array<ValidateType>;
}
interface UserData{
[key:string]:any
}
interface BlurDataType{
[key:string]:boolean
}
interface FnObjType{
[key:string]:(e:any)=>void
}
interface ValidataType{
[key:string]:string[]
}
type UseFormType=[(fn: any) => void,FnObjType,ValidataType,FnObjType]
function useForm(args:UseFormProps[]):UseFormType{
const [state,setState]=useState<UserData>()
const [validata,setValidata]=useState<ValidataType>(()=>args.reduce((p,n)=>{p[n.name]=[];return p},{} as ValidataType))
const [blurData,setBlurData]=useState<BlurDataType>({})
const returnObj=useMemo(()=>{
let obj:FnObjType={}
let blurobj:FnObjType={}
args.forEach((o)=>{
obj[o.name]=(e:any)=>{
if(o.validate){
let resArr:string[]=[]
o.validate.forEach((v)=>{
let sign = v.validate(e)
if(!sign){
resArr.push(v.message)
}
})
setValidata({...validata,...{[o.name]:resArr}})
}
setState({...state,...{[o.name]:e}})
}
blurobj[o.name]=(e:any)=>{
if(blurData&&blurData[o.name]){
}else{
setBlurData({...blurData,...{[o.name]:true}})
if(o.validate){
let resArr:string[]=[]
o.validate.forEach((v)=>{
let sign = v.validate(e)
if(!sign){
resArr.push(v.message)
}
})
setValidata({...validata,...{[o.name]:resArr}})
}
}
}
})
return[obj,blurobj]
},[args, blurData, state, validata])
const handleSubmit=(fn:any)=>{
fn(state)
}
return [handleSubmit,returnObj[0],validata,returnObj[1]]
}
export default useForm;