【React】手寫並封裝受控組件自定義hook表單驗證器(附組件庫線上體驗地址)

前言

  • 有小夥伴說寫組件庫封裝表單沒有思路。我一開始感覺確實有點不太好寫,如果自己寫了自己用也就算了,做成封裝給別人用的那考慮東西就有點多。後來有人發了個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){//true驗證過
                            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){//true驗證過
                                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;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章