レビュー済み·難易度: 中級·更新: 2026-04-14

Next.js 15 + React Hook Form + Zod でログインフォームを作る

Next.js App Router 環境で react-hook-form と zod を使ったログインフォームの実装例。クライアントバリデーションとエラー表示を型安全に扱う。

nextjsformvalidationauthenticationreact-hook-formzodtailwindcss

対応バージョン

nextjs 15react 19typescript 5react-hook-form 7zod 3tailwindcss 4

前提環境

Next.js App Router の基本的な使い方を理解していること

概要

react-hook-formzod を組み合わせることで、型安全なフォームバリデーションを実現する。 zodResolver を使うことでスキーマ定義とフォームロジックを分離できる。

インストール

npm install react-hook-form @hookform/resolvers zod

スキーマ定義

// src/lib/schemas/login.ts
import { z } from "zod";

export const loginSchema = z.object({
  email: z.string().email("有効なメールアドレスを入力してください"),
  password: z.string().min(8, "パスワードは 8 文字以上で入力してください"),
});

export type LoginInput = z.infer<typeof loginSchema>;

フォームコンポーネント

// src/components/LoginForm.tsx
"use client";

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { loginSchema, type LoginInput } from "@/lib/schemas/login";

export function LoginForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<LoginInput>({
    resolver: zodResolver(loginSchema),
  });

  const onSubmit = async (data: LoginInput) => {
    // TODO: API 呼び出し
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4 max-w-sm">
      <div>
        <label htmlFor="email" className="block text-sm font-medium">
          メールアドレス
        </label>
        <input
          id="email"
          type="email"
          {...register("email")}
          className="mt-1 block w-full rounded border px-3 py-2 text-sm"
        />
        {errors.email && (
          <p className="mt-1 text-xs text-red-600">{errors.email.message}</p>
        )}
      </div>

      <div>
        <label htmlFor="password" className="block text-sm font-medium">
          パスワード
        </label>
        <input
          id="password"
          type="password"
          {...register("password")}
          className="mt-1 block w-full rounded border px-3 py-2 text-sm"
        />
        {errors.password && (
          <p className="mt-1 text-xs text-red-600">{errors.password.message}</p>
        )}
      </div>

      <button
        type="submit"
        disabled={isSubmitting}
        className="w-full rounded bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
      >
        {isSubmitting ? "ログイン中..." : "ログイン"}
      </button>
    </form>
  );
}

ポイント

  • zodResolver でスキーマとフォームを接続する
  • errors は Zod のエラーメッセージを型安全に参照できる
  • isSubmitting で二重送信を防止する

注意点

Server Actions と組み合わせる場合は server-actions サンプルも参照すること

関連サンプル