如果問有什麼是前端菜雞一定要會、要做的,表單驗證絕對榜上有名,如果不做,不只被餵了一堆垃圾資料的後端同事會生氣,從負責企劃流程與使用者體驗的 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)