React Hook Form 高效能表單驗證套件
在 React 中處理表單,傳統的受控元件 (Controlled Components) 方式(即為每個 input 綁定 state)在表單欄位變多時,會導致大量的 re-renders,造成效能問題。
React Hook Form 採取不同的策略,它主要依賴非受控元件 (Uncontrolled Components) 的 ref 機制,加上它獨特的訂閱與隔離重渲染技術,能夠在不影響效能的情況下處理複雜的表單驗證。
安裝
npm install react-hook-form
基本用法 (Basic Usage)
使用 useForm Hook 是起手式。你需要用到三個核心屬性:
- register: 這是最核心的函式。它的作用是將 DOM 元素(通常是
<input>)註冊到 Hook Form 中,並回傳必要的屬性(如onChange,onBlur,name,ref)。- 語法:
{...register("欄位名稱", { 驗證規則 })} - 用法:你需要使用 Spread Operator (
...) 將其展開到 input 元素上。
- 語法:
- handleSubmit: 這個函式用來處理表單提交。它接收兩個參數:
onSubmit(必填):當驗證通過時執行的 callback,參數會是表單的資料物件。onError(選填):當驗證失敗時執行的 callback。- 用法:放在
<form>的onSubmit屬性中,例如:<form onSubmit={handleSubmit(onSubmit)}>。
- formState: 這是一個物件,包含了表單當前的所有狀態資訊。
- 常見屬性:
errors(錯誤訊息),isDirty(是否被修改過),isSubmitting(是否正在提交),isValid(是否通過驗證)。 - 注意:React Hook Form 使用 Proxy 來優化效能,只有你有「讀取」到的屬性改變時,才會觸發重新渲染。
- 常見屬性:
import { useForm } from 'react-hook-form';
export default function App() {
const {
register,
handleSubmit,
watch,
formState: { errors },
} = useForm();
const onSubmit = (data) => console.log(data);
// 監聽特定欄位(如果需要在渲染時顯示即時值)
console.log(watch('example'));
return (
/* "handleSubmit" 會在呼叫 "onSubmit" 之前驗證你的輸入 */
<form onSubmit={handleSubmit(onSubmit)}>
{/* 註冊 input:register('欄位名稱') */}
<input defaultValue="test" {...register('example')} />
{/* 加上驗證規則:required: true */}
<input {...register('exampleRequired', { required: true })} />
{/* 顯示錯誤訊息 */}
{errors.exampleRequired && <span>此欄位必填</span>}
<input type="submit" />
</form>
);
}
驗證規則 (Validation)
register 的第二個參數可以用來定義驗證規則。支援標準的 HTML 驗證屬性:
- required
- min / max
- minLength / maxLength
- pattern
- validate (自訂驗證函式)
<input
{...register('firstName', {
required: '請輸入名字', // 可以直接是錯誤字串
maxLength: {
value: 20,
message: '名字不能超過 20 個字元',
},
pattern: {
value: /^[A-Za-z]+$/i,
message: '只能輸入字母',
},
})}
/>;
{
/* 顯示定義好的錯誤訊息 */
}
<p>{errors.firstName?.message}</p>;
整合 UI Library
如果你使用 Material UI、Ant Design 等第三方 UI 庫,它們的 input 可能不容易直接使用 ...register(因為 ref 的轉發問題)。這時可以使用 Controller 元件。
Controller 常用屬性
Controller 是一個 Wrapper 元件,幫你處理了註冊和與外部 UI 庫的溝通。
- name (必填):欄位的唯一名稱,用來當作輸出的 key。
- control (必填):從
useForm回傳的control物件。 - render (必填):用來渲染 UI 元件的 render prop。它會接收
field和fieldState。field: 包含onChange,onBlur,value,ref,你需要把這些屬性傳遞給你的 UI 元件。fieldState: 包含invalid,isTouched,isDirty,error,用來顯示錯誤狀態。
- rules (選填):驗證規則,格式與
register的第二個參數相同。
import { useForm, Controller } from 'react-hook-form';
import TextField from '@mui/material/TextField';
function MyForm() {
const { control, handleSubmit } = useForm({
defaultValues: {
firstName: '',
},
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<Controller
name="firstName"
control={control}
rules={{ required: true }}
render={({ field }) => <TextField {...field} label="First Name" variant="outlined" />}
/>
<input type="submit" />
</form>
);
}
Schema Validation (搭配 Zod)
雖然 React Hook Form 內建基本驗證,但對於複雜的資料結構驗證,搭配 Schema Library(如 Zod 或 Yup)是最佳實踐。這需要安裝 @hookform/resolvers。
npm install zod @hookform/resolvers
Zod 常用語法
Zod 是一個以 TypeScript 為優先的 Schema 宣告與驗證庫。它的語法非常直觀,幾乎就像是在寫 TypeScript 的 interface。
- 基礎型別:
z.string(),z.number(),z.boolean(),z.date() - 常用驗證:
.min(5, "錯誤訊息")/.max(10).email("Email 格式錯誤").url().optional()(設為非必填).nullable()(允許 null)
- 物件與推斷:
z.object({ ... }): 定義物件結構z.infer<typeof schema>: 自動從 schema 推斷出 TypeScript 型別
import { z } from 'zod';
// 定義 Schema
const UserSchema = z.object({
username: z.string().min(3, '使用者名稱至少需 3 個字元'),
email: z.string().email('請輸入有效的 Email'),
age: z.number().min(18).optional(), // 選填
website: z.string().url().nullable(), // 可以是 null
});
// 自動推斷 TypeScript 型別
type User = z.infer<typeof UserSchema>;
應用範例:
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
// 1. 定義 Schema
const schema = z.object({
username: z.string().min(1, { message: '必填' }),
age: z.number().min(18, { message: '必須年滿 18 歲' }),
email: z.string().email({ message: 'Email 格式錯誤' }),
});
export default function schemaForm() {
// 2. 將 resolver 傳入 useForm
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: zodResolver(schema),
});
return (
<form onSubmit={handleSubmit((d) => console.log(d))}>
<input {...register('username')} />
<p>{errors.username?.message}</p>
{/* 注意:input type="number" 預設也是字串,需使用 valueAsNumber */}
<input type="number" {...register('age', { valueAsNumber: true })} />
<p>{errors.age?.message}</p>
<input {...register('email')} />
<p>{errors.email?.message}</p>
<input type="submit" />
</form>
);
}
監聽數值 (Watching Values)
watch 的作用就像是為表單欄位裝上了監視器。當你想要根據使用者的輸入即時改變 UI(例如:勾選 "顯示更多" 才出現額外欄位,或是即時預覽輸入內容)時,就需要使用它。
它跟 getValues() 的最大差別在於:watch 會訂閱輸入變化並觸發元件重新渲染 (Re-render),而 getValues() 只會默默地讀取當前值,不會影響 UI。
// 監聽單一欄位
const watchShowAge = watch('showAge');
// 監聽多個欄位
const watchAllFields = watch(['firstName', 'lastName']);
// 監聽所有欄位
const watchAll = watch();
return (
<>
<input type="checkbox" {...register('showAge')} />
{/* 根據 showAge 的值動態顯示 */}
{watchShowAge && <input type="number" {...register('age')} />}
</>
);
手動操作 (Manual Operations)
有時候我們需要手動設定或讀取表單值,而不是透過 input onChange。
- setValue: 設定欄位值。
- getValues: 讀取欄位值 (不會觸發 re-render)。
- reset: 重置表單值。
const { setValue, getValues, reset } = useForm();
// 設定值 (shouldValidate: true 代表設定後立即觸發驗證)
setValue('firstName', 'Mike', { shouldValidate: true });
// 讀取值
const values = getValues(); // { firstName: 'Mike', ... }
// 重置表單
reset({ firstName: 'New Name' });
表單狀態 (Form State)
formState 物件包含了表單的各種狀態資訊,這對於 UX 非常重要(例如在提交中禁用按鈕)。
const {
formState: {
isDirty, // 使用者是否修改過任何欄位
isValid, // 表單是否通過所有驗證
isSubmitting, // 是否正在提交中 (handleSubmit 的 callback 執行期間)
isSubmitSuccessful, // 是否提交成功
submitCount, // 提交次數
},
} = useForm({ mode: 'onChange' }); // mode: 'onChange' 會讓 isValid 即時更新
return (
<button disabled={!isDirty || !isValid || isSubmitting}>
{isSubmitting ? '提交中...' : '送出'}
</button>
);
動態表單 (Dynamic Arrays)
對於陣列型態的欄位(例如:新增多個聯絡人),使用 useFieldArray。它提供了 append, prepend, remove, swap, move 等方法。
import { useForm, useFieldArray } from 'react-hook-form';
function App() {
const { register, control, handleSubmit } = useForm({
defaultValues: {
test: [{ name: 'test', quantity: 1 }],
},
});
const { fields, append, remove } = useFieldArray({
control,
name: 'test',
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<ul>
{fields.map((item, index) => (
<li key={item.id}>
{/* 重要:使用 index 來註冊 */}
<input {...register(`test.${index}.name`)} />
<input type="number" {...register(`test.${index}.quantity`)} />
<button type="button" onClick={() => remove(index)}>
Delete
</button>
</li>
))}
</ul>
<button type="button" onClick={() => append({ name: 'appendBill', quantity: 2 })}>
Append
</button>
<input type="submit" />
</form>
);
}
總結
React Hook Form 提供了更好的效能和更簡潔的 API,特別是當你需要處理大量欄位或是需要與後端驗證邏輯(透過 Zod)共用 Schema 時,它是一個非常強大的選擇。