🐣

React Hook Form、Zodで職務経歴書登録フォーム開発(バリデーション編)

2023/10/05に公開

前回行ったこと

前回は React Hook Form、Zodで職務経歴書登録フォーム開発(フォーム編) としてフォーム開発を行いました。今回は Zod を利用しフォームのバリデーションを行います。

今回使用したライブラリーのバージョン

  • Next.js: 13.4.19
  • React: 18.2.0
  • React Hook Form: 7.47.0
  • Zod: 3.22.4
  • MUI: 5.14.11
  • TailwindCSS: 3.3.3
  • Recoil: 0.7.7
  • TypeScript: 5.2.2
  • Firebase: 10.4.0

今回行ったことまとめ

  • 個人情報を入力するフォームにクライアントサイドバリデーションを実装

実装内容

【React Hook Form × Zod × MUI】フォームを作ろう! を参考に実装しました。

スキーマ定義

以下のようにスキーマを定義し、そこからPersonalInfo型を作成するようにしました。ポイントは以下です。

  • エラーメッセージもここで定義しています
  • dateOfBirthは string | dayjs.Dayjs のunion型としたかったのですが、その場合は z.union を利用するといいようでした
  • zipCodeに対しては正規表現でのバリデーションを行っています
    • より複雑なバリデーションを実装する場合は refine を利用するといいようです
types.ts
import dayjs from "dayjs";
import { z } from "zod";

export const PersonalInfoSchema = z.object({
  fullName: z.string().min(1, { message: "mandatory" }),
  email: z
    .string()
    .min(1, { message: "mandatory" })
    .email({ message: "should be in the format something@domain.com" }),
  dateOfBirth: z.union([
    z.string().min(1, { message: "mandatory" }),
    z.instanceof(dayjs as unknown as typeof dayjs.Dayjs),
  ]),
  zipCode: z
    .string()
    .min(1, { message: "mandatory" })
    .regex(/^\d{3}-\d{4}$/, { message: "should be in the format 000-0000" }),
  address: z.string().optional(),
});

export type PersonalInfo = z.infer<typeof PersonalInfoSchema>;

@hookform/resolvers のインストール

このあとフォームのバリデーションを行うのに便利な @hookform/resolvers をインストールします。

npm install @hookform/resolvers

useFormのオプション設定

以下のようにオプションを設定しました。各オプションの意味はコメントとして書いていますが、詳細は公式サイトをご確認下さい。

https://react-hook-form.com/docs/useform

const {
    register,
    handleSubmit,
    watch,
    formState: { errors },
    control,
    setValue,
  } = useForm<PersonalInfo>({
    // modeをonSubmitにすることで、Saveボタンを押すまではフォーカスアウトのタイミングではバリデーションは動かないようになる
    mode: "onSubmit",
    // reValidateModeをonBlurにすることで、Saveボタンが押された後はフォーカスアウトのタイミングでバリデーションが走る
    reValidateMode: "onBlur",
    // デフォルト状態はフォーム要素全てが未定義(undefined)の状態として取り扱う
    defaultValues: undefined,
    // zodResolverの引数にvalidation時に実行するschemaを渡す
    resolver: zodResolver(PersonalInfoSchema),
  });

フォーム内の各フィールドへのエラーメッセージの設定

基本的にはerrorとhelperTextを設定するだけで済みます。

<TextField
  variant="outlined"
  label="Full name"
  required
  {...register("fullName")}
  error={!!errors.fullName}
  helperText={errors.fullName?.message}
/>

DatePickerは少し工夫しています。

  • DatePicker自体にerrorやhelperTextのプロパティは存在しないため、子供のtextFieldのプロパティとして設定しています
  • 同様にonBlurも子供のtextFieldのプロパティとして設定しています。これを設定しないとフォーカスアウトのタイミングでバリデーションが走りません
  • また、日付を入力する際にDatePicker(カレンダーUI)から日付を選択すると、その後フォーカスアウトしてもonBlurイベントが発火しないようでした。そのためonChangeの中で field.onBlur(); を実行しています
<Controller
  name="dateOfBirth"
  control={control}
  render={({ field }) => (
    <DatePicker
      label="Date of birth"
      slotProps={{
        textField: {
          required: true,
          error: !!errors.dateOfBirth,
          helperText: errors.dateOfBirth?.message,
          onBlur: field.onBlur,
        },
        field: { clearable: true },
      }}
      {...field}
      onChange={(value) => {
        const _value = value === null ? "" : value;
        field.onChange(_value);
        field.onBlur();
      }}
    />
  )}

コード

https://github.com/shoji9x9/firebase-nextjs

アプリケーション

https://nextjs-a609c.web.app/

職務経歴書登録フォーム開発の総括(中間)

実はまだ個人情報を登録できるようにしただけで職務経歴を登録できるようになっていないのでタイトル詐欺継続中なのですが、一旦ここまでを振り返りたいと思います。

Discussion