Zod × React Hook Formでリアルタイムバリデーションを実装する

〜Next.js環境での型安全なフォーム体験〜

はじめに

フォームの開発では、「入力中にすぐエラーを見せたい」「入力チェックを型安全に書きたい」といったニーズがあります。
React Hook Form(以下RHF)とZodを組み合わせることで、リアルタイムかつ型安全なバリデーションをシンプルに実装できます。

この記事では、Next.js + TypeScript環境でのZod × RHF連携方法を具体的に紹介します。

セットアップ

まず必要なパッケージをインストールします。

pnpm add react-hook-form @hookform/resolvers zod

スキーマ定義

まずはZodでフォーム入力のスキーマを定義します。

// src/lib/validators/contactSchema.ts
import { z } from "zod";

export const contactSchema = z.object({
  name: z.string().min(1, "名前は必須です"),
  email: z.string().email("正しいメールアドレスを入力してください"),
  message: z.string().min(10, "10文字以上入力してください"),
});

export type ContactInput = z.infer<typeof contactSchema>;

z.infer により、Zodスキーマから型定義を自動生成できます。
この型をRHFにそのまま渡せるのがポイントです。

React Hook Formで利用する

RHFのuseFormを使い、Zodスキーマをバリデーションルールとして適用します。

// src/app/contact/page.tsx
"use client";

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { contactSchema, ContactInput } from "@/lib/validators/contactSchema";

export default function ContactPage() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<ContactInput>({
    resolver: zodResolver(contactSchema),
    mode: "onChange", // 入力変更時にリアルタイム検証
  });

  const onSubmit = (data: ContactInput) => {
    console.log("送信データ:", data);
    alert("送信完了!");
  };

  return (
    <form
      onSubmit={handleSubmit(onSubmit)}
      className="max-w-md mx-auto flex flex-col gap-4 mt-8"
    >
      <div>
        <label className="block text-sm font-medium">名前</label>
        <input {...register("name")} className="border p-2 w-full" />
        {errors.name && <p className="text-red-500">{errors.name.message}</p>}
      </div>

      <div>
        <label className="block text-sm font-medium">メール</label>
        <input {...register("email")} className="border p-2 w-full" />
        {errors.email && <p className="text-red-500">{errors.email.message}</p>}
      </div>

      <div>
        <label className="block text-sm font-medium">メッセージ</label>
        <textarea {...register("message")} className="border p-2 w-full h-24" />
        {errors.message && <p className="text-red-500">{errors.message.message}</p>}
      </div>

      <button
        type="submit"
        disabled={isSubmitting}
        className="bg-blue-600 text-white py-2 rounded disabled:opacity-50"
      >
        送信
      </button>
    </form>
  );
}

zodResolver を設定するだけで、ZodのスキーマがRHFに統合され、リアルタイムバリデーションが自動的に実行されます。

よくあるミス:use client の付け忘れ

RHFはクライアント側で動作するため、フォームコンポーネントの冒頭に use client を付ける必要があります。
これを忘れると useForm フックがエラーになります。

UX向上のためのヒント

  1. mode: "onChange" または "onBlur" を活用する
    入力時点でバリデーションを走らせることで、送信前にエラーを即時表示できます。

  2. エラーをまとめて表示する
    formState.errors を活用して、複数項目のエラーを一覧で表示することも可能です。

  3. 型の再利用
    ContactInput 型をServer ActionsやAPIハンドラでも再利用することで、フロントとバックの整合性を保てます。

まとめ

Zod × React Hook Formを組み合わせることで、

を実現できます。

次回は、このフォームをServer Actionsと統合し、クライアントからサーバーまで型安全にデータを流す構成を紹介します。