🐣
React Hook Form、Zodで職務経歴書登録フォーム開発(バリデーション編)
前回行ったこと
前回は 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型を作成するようにしました。ポイントは以下です。
- エラーメッセージもここで定義しています
- 今回は利用しませんでしたが、 Customizing errors with ZodErrorMap を利用すればより複雑なエラーメッセージも定義できそうです
- 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のオプション設定
以下のようにオプションを設定しました。各オプションの意味はコメントとして書いていますが、詳細は公式サイトをご確認下さい。
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();
}}
/>
)}
コード
アプリケーション
職務経歴書登録フォーム開発の総括(中間)
実はまだ個人情報を登録できるようにしただけで職務経歴を登録できるようになっていないのでタイトル詐欺継続中なのですが、一旦ここまでを振り返りたいと思います。
- React Hook Formは簡単にフォームを作成できたので今後も使っていきたい
- Zodも設定が簡単だしバリデーションの統一も実現しやすそうなので今後も使っていきたい
- Next.jsのApp Router周り(サーバー/クライアントコンポーネント含む)がうまく実装できていない気がするのでじっくり勉強したい
- Next.js + TailwindCSS の場合のUIコンポーネントは何がいいのか考えたい。ヘッドレスUIコンポーネントも気になる
- Next.js + Firebase の構成は現時点では フレームワーク対応の Hosting は早期公開プレビュー版です とのことなので、特に理由がなければホスティング先は Vercel などにしておいた方が無難そう。AuthenticationやFirestoreは便利なので今後も使いたい
- 残課題:Firestoreの設定などが自動デプロイできていない も解決したい
- FirebaseのFunctionsやWebプッシュ機能も使ってみたい
- 他にもAWSの勉強したり新しいパソコンのセットアップしたり・・・。(仕事してないのに)忙しい!
Discussion