概要
react-hook-form と zod を組み合わせることで、型安全なフォームバリデーションを実現する。
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で二重送信を防止する