前端工程

Article

React Hook Form v7 + Zod v4 表單套件篇

以 React Hook Form 管理表單狀態,搭配 Zod Schema 驗證,整理 resolver、巢狀欄位與可控元件實作。


概述

各位未來前端棟樑你們好,歡迎來到 「React 專案表單套件篇」,此篇在介紹當遇到中大型表單或者覺得表單狀態難以掌控時我們可以使用更方便的套件幫忙管理。 本篇章會使用目前常見的兩種套件做搭配使用( 如標題 ),但我們先來釐清一起這兩者套件的關係 :

兩者套件都能獨立做使用,而 RHF 提供第三方的 Schema Validation 套件進行組合使用,這樣就能做出更彈性的表單驗證自訂邏輯。 React-Hook-Form (RHF): 基於 React Hook 所打造的表單狀態管理,可進行統一管理、提供不同狀態及設置方式,並且可優化 React 表單狀態下在 re-render 問題。 Zod: Zod 是以 TypeScript 型別系統為核心的 schema 驗證函式庫,可簡單定義資料結構,並在執行期間進行型別安全的驗證。以下用範例來簡述 :


RHF + Zod 流程介紹(同時使用)

這邊就直接不贅述它們各自的使用方式(有興趣的話下方章節有簡單介紹)。

resolver:可以當成 RHF 用來解析外部驗證器的 middleware,將表單驗證結果轉換成符合 RHF 格式的表單值。

補充小知識:早期 RHF 在實作第三方的 resolver 時是針對各自套件所客製化撰寫的,但近年來這種不便性也隨之而來,所以套件方的作者就一起討論出了一種 Type 標準,稱之「Standard-schema」,只需要由第三方的套件去複製這 Type 檔就可以讓 TypeScript 系統成功推論(而該檔案的程式碼行數也非常之少呢!)

我有興趣看「Standard-schema」原始碼 以下就來看看怎麼結合使用吧!


Zod v4

Zod 是以 TypeScript 為核心的 schema 驗證函式庫,讓你用簡潔的 API 定義資料結構,並在執行期間進行型別安全的驗證,使用方式建議直接看官方文件會更明白( 絕不是因為偷懶 )。點我查看

Zod 常見問題

1. 我想統一自訂錯誤訊息該怎麼辦?

很好,你很細心的想要極致客製化錯誤訊息,不愧是未來前端棟樑,官方提供兩種做法。

  • 透過官方的 Locales 翻譯檔案直接偷懶
  • 使用自訂方法判定錯誤訊息 ( 你應該會選這方式...應該啦 )

React Hook Form v7

  • useForm hook 為入口,管理表單狀態、驗證、錯誤
  • 提供 registerControlleruseFieldArray 等工具應對各種場景
  • 支援外部 schema 驗證函式庫,透過 resolver 接入

useForm 參數說明(常用)

參數預設值說明
modeonSubmit驗證觸發時機(送出前)。onChange 每次輸入觸發、onBlur 離開欄位觸發、onSubmit 送出時觸發、onTouched 第一次 blur 後觸發、all 全部
reValidateModeonChange驗證觸發時機(送出後,有錯誤的欄位重新驗證時機)。可選 onChangeonBluronSubmit
defaultValues{}表單初始值,支援同步或非同步(async function)。會被 cache,重置要用 reset()
values從外部狀態或 server 動態更新表單值,每次變動都會同步進表單
resolverundefined接入外部 schema 驗證(Zod、Yup 等),透過 @hookform/resolvers 提供
criteriaModefirstError錯誤收集模式。firstError 只回傳第一個錯誤、all 回傳所有錯誤(存在 errors.field.types
shouldFocusErrortrue送出驗證失敗時,是否自動 focus 到第一個有錯誤的欄位
shouldUnregisterfalse元件 unmount 時是否自動移除該欄位的值與驗證。true 行為接近原生表單
delayErrorundefined錯誤訊息延遲顯示的毫秒數,避免輸入中途就顯示錯誤

useForm 回傳值說明(常用)

回傳值說明
register註冊欄位,回傳 refonChangeonBlurname 供 input 使用,可附加驗證規則
unregister取消註冊欄位,移除對應的值與驗證
handleSubmit包裝 submit handler,驗證通過才呼叫 callback,失敗則將錯誤寫入 formState.errors
watch訂閱欄位值變動,每次變動都會 re-render,適合用於條件渲染
getValues讀取當前表單值,不訂閱變動、不觸發 re-render,適合在事件中取值
setValue程式化設定欄位值,可選擇是否觸發驗證、標記 dirty 或 touched
setError手動設定欄位錯誤,常用於 server 回傳驗證錯誤
clearErrors清除指定欄位或全部欄位的錯誤
reset重置整個表單狀態與值,可傳入新值或保留部分狀態
resetField重置單一欄位的值與狀態
trigger手動觸發指定欄位或全部欄位的驗證
control傳給 ControlleruseFieldArrayuseWatch 等需要存取表單內部的元件
formState表單整體狀態物件(見下方)

formState(常用)

屬性說明
errors各欄位的錯誤物件,key 為欄位名,value 含 messagetype
isValid所有欄位驗證通過為 true
isSubmitting表單正在送出中(handleSubmit callback 執行期間)為 true
isSubmitted表單曾被送出過為 true,reset 後才會還原
isSubmitSuccessful送出成功(無 runtime 錯誤)為 true
isLoading非同步 defaultValues 載入中為 true

formState 內部用 Proxy 實作,只有實際解構取用的屬性才會訂閱更新,未使用的屬性不會觸發 re-render。


實務小技巧

1. 減少傳遞過多層的 React 元件來使用 useForm 相關方法

我們在正常開發時元件不會像範例只有一層,會遇到多層結構等等,我們可以使用官方提供的 FormProvideruseFormContext 來取得,可以把這認定為 React 的 Context Provider 機制,但兩者使用上還是有差異的。

createContextFormProvider
角色React 底層 API基於 createContext 的封裝
狀態管理自己決定放什麼固定放 useForm 回傳的所有方法
消費方式useContext(MyContext)useFormContext()
彈性完全自由只能用在 RHF 表單
適用場景任何需要跨層傳遞資料的情境深層子元件需要存取表單方法時

2. 我想建立 Reusable 的可控表單元件

官方也提供 useController 讓你自訂可重用的 RHF 表單元件,只需傳入 controlname 就可完成。

3. 我想用別的方式取得 formState 值

你可以透過 useFormState 對某個欄位值狀態進行訂閱監聽,而此方式監聽也能減少 re-render 問題,因訂閱狀態只限制在 useFormState 這個 scope 底下。


RHF 常見問題

1. 如果我的 Schema 有定義巢狀欄位呢?

2. 如果我的 Schema 有定義陣列欄位呢?

透過官方提供的 useFieldArray 先將 useForm 給予的 control 丟給該 hook 再指定 name 欄位名稱進行綁定,回傳方法 點我查看(其實就跟普通陣列操作一樣,這裡就不贅述)。

3. 我發現某些第三方元件儘管透過 register 訂閱後表單狀態值還是不會改變?

有些第三方元件不提供 ref 的方式來控制元件(register 就是透過 ref 接管 input 值),不過 RHF 也有提供相關方式解決這問題,官方提供 Controller wrapper 元件,讓你把第三方或自己客製的元件包覆起來,並提供 props 讓您自行呼叫表單更新。點我查看

4. 我發現我使用 useForm 的 watch 監聽欄位時我的元件會發生 re-render 問題

不錯!你看到這問題的話果然是未來前端棟樑,官方提供了 useWatch 的 hook 讓你可以監聽欄位值變化,不同的是這 hook 也可以防止元件 re-render 問題。點我查看


其他補充

RHF 支援的 Schema 套件(常見)

透過 @hookform/resolvers 套件提供各種 schema 的 resolver,目前常見的為以下,想看更多其他支援可以點我查看

套件resolver
ZodzodResolver
YupyupResolver
JoijoiResolver
ValibotvalibotResolver

參考文件