如果問有什麼是前端菜雞一定要會、要做的,表單驗證絕對榜上有名,如果不做,不只被餵了一堆垃圾資料的後端同事會生氣,從負責企劃流程與使用者體驗的 PM 和設計師,到負責驗測的 QA 和每天提心吊膽怕漏洞被鑽的資安工程師,甚至其他的前端都會白眼翻到後腦勺,還保不定所有人都想灌你一拳。
在昨天的範例中可以看到,React Hook Form 也有提供表單驗證,驗證規則可以直接設定在欄位上,這邊總結一下:
-非受控元件:使用 register,將驗證規則作為 register 的第二個參數
-受控元件:使用 Controller,將驗證規則放在 rules 屬性
這邊可以看看 React Hook Form 提供的驗證方法,我這邊舉出幾個我比較常使用的方法:
import { useForm, Controller } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
export default function Form() {
const {
register,
handleSubmit,
control,
formState: { errors },
} = useForm();
const onSubmit = (data: FormValues) => console.log("表單送出:", data);
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4 max-w-md mx-auto">
{/* 非可控 */}
<Input
id="username"
{...register("username", {
required: "必填",
minLength: { value: 3, message: "至少 3 個字元" },
maxLength: { value: 10, message: "最多 10 個字元" },
pattern: { value: /^[A-Za-z0-9]+$/, message: "僅限英文與數字" },
})}
/>
{errors.username &&
<p className="text-red-500 text-sm">{errors.username.message}</p>}
{/* 非可控 (數字驗證) */}
<Input
id="age"
type="number"
{...register("age", {
required: "必填",
min: { value: 18, message: "必須 ≥ 18" },
max: { value: 60, message: "必須 ≤ 60" },
validate: v => (v % 2 === 0 ? true : "年齡必須是偶數"),
})}
/>
{errors.age &&
<p className="text-red-500 text-sm">{errors.age.message}</p>}
{/* 可控 */}
<Controller
name="role"
control={control}
rules={{ required: "必選" }}
render={({ field }) => (
<Select onValueChange={field.onChange} value={field.value}>
<SelectTrigger>
<SelectValue placeholder="選擇角色" /></SelectTrigger>
<SelectContent>
<SelectItem value="admin">管理員</SelectItem>
<SelectItem value="user">使用者</SelectItem>
</SelectContent>
</Select>
)}
/>
{errors.role &&
<p className="text-red-500 text-sm">{errors.role.message}</p>}
<Button type="submit">送出</Button>
</form>
);
}
寫到這裡,雖然 React Hook Form 不錯用,但缺點也顯而易見吧?那就是所有的驗證規則散落在每個欄位上,如果是個大表單,相信維運的人火氣應該又上來了吧?
其實,React Hook Form 自己也知道這個短處,所以提供了與其他驗證套件整合的方法,來達到集中管理驗證邏輯的目的,這邊用 yup 作示範:
安裝 resolver:除了安裝好 React Hook Form,也需安裝 resolver 和 要使用的驗證套件:
npm install @hookform/resolvers yup
利用 yup 集中書寫驗證規則,讓 yupResolver 指定該驗證集合,並在 useForm 中的 resolver 帶入 yupResolver:
import { useForm, Controller } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
// 定義 Yup 驗證 Schema
const schema = yup.object({
username: yup
.string()
.required("必填")
.min(3, "至少 3 個字元")
.max(10, "最多 10 個字元")
.matches(/^[A-Za-z0-9]+$/, "僅限英文與數字"),
age: yup
.number()
.typeError("必須是數字")
.required("必填")
.min(18, "必須 ≥ 18")
.max(60, "必須 ≤ 60")
.test("even", "年齡必須是偶數", (v) => (v ?? 0) % 2 === 0),
role: yup.string().required("必選"),
});
export default function Form() {
const {
register,
handleSubmit,
control,
formState: { errors },
} = useForm({
resolver: yupResolver(schema), // 使用 yup 驗證
defaultValues: { username: "", age: undefined, role: "" },
});
const onSubmit = (data: FormValues) => console.log("表單送出:", data);
return (
<form
onSubmit={handleSubmit(onSubmit)}
className="space-y-4 max-w-md mx-auto"
>
{/* 非可控 - username */}
<Input id="username" {...register("username")} />
{errors.username && (
<p className="text-red-500 text-sm">{errors.username.message}</p>
)}
{/* 非可控 - age */}
<Input id="age" type="number" {...register("age")} />
{errors.age && (
<p className="text-red-500 text-sm">{errors.age.message}</p>
)}
{/* 可控 - role */}
<Controller
name="role"
control={control}
render={({ field }) => (
<Select onValueChange={field.onChange} value={field.value}>
<SelectTrigger>
<SelectValue placeholder="選擇角色" />
</SelectTrigger>
<SelectContent>
<SelectItem value="admin">管理員</SelectItem>
<SelectItem value="user">使用者</SelectItem>
</SelectContent>
</Select>
)}
/>
{errors.role && (
<p className="text-red-500 text-sm">{errors.role.message}</p>
)}
<Button type="submit">送出</Button>
</form>
);
}
相同的表單,換成這樣寫是不是結構更清爽了呢?降低維運人員的血壓,你我有責,共勉之XD (怎麼感覺應該要當作目前比賽的標題XD若有參加下次鐵人賽,來試試好了XDD)