iT邦幫忙

2025 iThome 鐵人賽

DAY 11
0
自我挑戰組

請問這是魔法嗎?前端轉職菜雞的修煉之路!系列 第 11

DAY 11 防禦魔法 - React Hook Form + Yup 不做表單驗證是種戰爭罪

  • 分享至 

  • xImage
  •  

如果問有什麼是前端菜雞一定要會、要做的,表單驗證絕對榜上有名,如果不做,不只被餵了一堆垃圾資料的後端同事會生氣,從負責企劃流程與使用者體驗的 PM 和設計師,到負責驗測的 QA 和每天提心吊膽怕漏洞被鑽的資安工程師,甚至其他的前端都會白眼翻到後腦勺,還保不定所有人都想灌你一拳。

在昨天的範例中可以看到,React Hook Form 也有提供表單驗證,驗證規則可以直接設定在欄位上,這邊總結一下:
-非受控元件:使用 register,將驗證規則作為 register 的第二個參數
-受控元件:使用 Controller,將驗證規則放在 rules 屬性

這邊可以看看 React Hook Form 提供的驗證方法,我這邊舉出幾個我比較常使用的方法:

  • required:即必填
  • minLength:最小字元長度,使用物件中的 value 設置數值,message 設置錯誤訊息
  • maxLength:最大字元長度,使用物件中的 value 設置數值,message 設置錯誤訊息
  • pattern:正則表達式驗證,使用物件中的 value 設置正規表達式,message 設置錯誤訊息
  • min:最小數值,使用物件中的 value 設置數值,message 設置錯誤訊息
  • max:最大數值,使用物件中的 value 設置數值,message 設置錯誤訊息
  • validate:自訂驗證規則,可寫同步/非同步邏輯,支援多條件。
  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 作示範:

  1. 安裝 resolver:除了安裝好 React Hook Form,也需安裝 resolver 和 要使用的驗證套件:

    npm install @hookform/resolvers yup
    
  2. 利用 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)


上一篇
DAY 10 招喚表單魔法陣 - React Hook Form (3) 此受控非彼獸控的表單套件組合技 Controller
下一篇
DAY 12 施展地獄業火的魔法 - Firebase (1) Firestore Database
系列文
請問這是魔法嗎?前端轉職菜雞的修煉之路!14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言