React Hook Form + Zod の紹介と導入【Next.js】
はじめに
先日、React の勉強会で、フォーム実装について取り上げました 📝
フォーム処理とバリデーションは、Web アプリケーション開発において重要でありながら、意外と複雑になりがちな要素です。
今回は、React Hook Form と Zod について調査したので、基礎的な内容をまとめました!
時間の節約になれば、嬉しいです 🙌
React Hook Form とは?
React で、複雑なフォーム処理を、シンプルかつ効率的に実装できるのが、React Hook Form です。
フォーム状態の管理、バリデーション、エラーハンドリングなどを簡潔に実装でき、
不要な再レンダリングを抑制する設計になっています!
React Hook Form(以下、RHF)は、パフォーマンスを重視し、
自力で実装するよりもコード量を少なく済ませることができるのが特徴の一つです。
Zod とは?
Zod は、TypeScript ファーストのスキーマ検証(バリデーション)ライブラリです。
フォームのバリデーションルールを宣言的に定義でき、
React Hook Form と組み合わせることで、型安全なフォーム実装が可能になります!
Zod の特徴は以下の通りです:
- TypeScript との優れた連携
- 宣言的なバリデーションルール定義
- カスタムエラーメッセージの柔軟な設定
- 複雑なバリデーションにも対応(ネストやリスト、条件付きなど)
React Hook Form の必要性
そもそも、なぜ React Hook Form が必要なのでしょうか?
実際に、RHF を使わないフォーム実装と比較してみましょう。
RHF を使わないフォーム実装の例
従来の React でのフォーム実装は、以下のようになりがちです:
import { useState } from "react";
function ContactForm() {
// 各フィールドの状態管理
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [message, setMessage] = useState("");
// エラー状態管理
const [errors, setErrors] = useState({
name: "",
email: "",
message: "",
});
// 送信状態管理
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitStatus, setSubmitStatus] = useState("");
// バリデーション関数
const validateForm = () => {
let valid = true;
const newErrors = {
name: "",
email: "",
message: "",
};
// 名前のバリデーション
if (name.length < 2) {
newErrors.name = "お名前は2文字以上で入力してください";
valid = false;
}
// メールのバリデーション
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
newErrors.email = "有効なメールアドレスを入力してください";
valid = false;
}
// メッセージのバリデーション
if (message.length < 10) {
newErrors.message = "お問い合わせ内容は10文字以上で入力してください";
valid = false;
}
setErrors(newErrors);
return valid;
};
// 送信ハンドラ
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setIsSubmitting(true);
try {
// API呼び出し
const response = await fetch("/api/contact", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ name, email, message }),
});
const data = await response.json();
if (response.ok) {
setSubmitStatus("success");
// フォームリセット
setName("");
setEmail("");
setMessage("");
} else {
setSubmitStatus("error");
}
} catch (error) {
setSubmitStatus("error");
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">お名前</label>
<input
id="name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
{errors.name && <p className="error">{errors.name}</p>}
</div>
<div>
<label htmlFor="email">メールアドレス</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
{errors.email && <p className="error">{errors.email}</p>}
</div>
<div>
<label htmlFor="message">お問い合わせ内容</label>
<textarea
id="message"
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
{errors.message && <p className="error">{errors.message}</p>}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "送信中..." : "送信する"}
</button>
{submitStatus === "success" && (
<p className="success">お問い合わせを受け付けました。</p>
)}
{submitStatus === "error" && (
<p className="error">エラーが発生しました。再度お試しください。</p>
)}
</form>
);
}
上記のコードでは、以下のような課題があります:
- 冗長な状態管理:各フィールドに対して個別の state が必要
- 手動のバリデーション:バリデーションロジックを自前で実装する必要がある
- 再レンダリングの多発:各入力ごとに再レンダリングが発生
- TypeScript との統合が弱い:型安全性を確保するのが難しい
- テストの複雑さ:多くの状態と条件分岐があるため、テストが複雑になる
自力で全部やろうとすると途端に複雑になります。。
とはいえ、適切なバリデーションやメッセージの表示は、フォームの実装には重要なポイントです!
React Hook Form の導入手順(Next.js)
それでは、React Hook Form と Zod を使って、
同じフォームをより効率的に実装してみましょう!
以下の手順で導入していきましょう。
1. ライブラリのインストール
npm install react-hook-form @hookform/resolvers zod
これにより、以下のライブラリがインストールされます:
-
react-hook-form
: フォーム管理のコアライブラリ -
zod
: バリデーションスキーマライブラリ -
@hookform/resolvers
: React Hook Form と Zod を連携させるアダプター
2. バリデーションスキーマの定義
まず、Zod を使ってバリデーションルールを定義します:
import * as z from "zod";
// バリデーションスキーマの定義
export const contactSchema = z.object({
name: z
.string()
.min(2, { message: "お名前は2文字以上で入力してください" })
.max(50, { message: "お名前は50文字以内で入力してください" }),
email: z
.string()
.email({ message: "有効なメールアドレスを入力してください" }),
message: z
.string()
.min(10, { message: "お問い合わせ内容は10文字以上で入力してください" })
.max(1000, { message: "お問い合わせ内容は1000文字以内で入力してください" }),
});
// 型定義のエクスポート
export type ContactFormValues = z.infer<typeof contactSchema>;
このスキーマでは、各フィールドの入力条件を宣言的に定義しています:
- name: 2〜50 文字の名前
- email: 有効なメールアドレス形式
- message: 10〜1000 文字のお問い合わせ内容
Zod の強力な点は、スキーマから TypeScript の型を自動生成できることです。
z.infer<typeof contactSchema>
が、そのフォーム値の型定義になります。
3. React Hook Form の実装
次に、React Hook Form と定義したスキーマを使ってフォームを実装します:
import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { contactSchema, ContactFormValues } from "./contactSchema";
function ContactForm() {
const [submitStatus, setSubmitStatus] = useState<
"idle" | "submitting" | "success" | "error"
>("idle");
// React Hook Formの初期化
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm<ContactFormValues>({
resolver: zodResolver(contactSchema),
defaultValues: {
name: "",
email: "",
message: "",
},
});
// 送信処理
const onSubmit = async (data: ContactFormValues) => {
setSubmitStatus("submitting");
try {
// API呼び出し処理...
setSubmitStatus("success");
reset(); // フォームリセット
} catch (error) {
setSubmitStatus("error");
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="name">お名前</label>
<input id="name" {...register("name")} />
{errors.name && <p>{errors.name.message}</p>}
</div>
{/* email、messageフィールドも同様 */}
<button type="submit" disabled={submitStatus === "submitting"}>
{submitStatus === "submitting" ? "送信中..." : "送信する"}
</button>
{/* 成功・エラーメッセージ */}
</form>
);
}
このコードでは、React Hook Form の主要な機能を活用しています:
-
useForm
フックでフォームを初期化 -
zodResolver
を使って Zod スキーマと連携 -
register
関数で入力フィールドを登録 -
handleSubmit
で送信処理をラップ(バリデーション成功時のみ実行) -
reset
でフォームの値をリセット -
formState.errors
でバリデーションエラーにアクセス
React Hook Form の主なメリットは?
このように実装することで、以下のようなメリットが得られます:
- コード量の削減:状態管理やバリデーションのボイラープレートコードが大幅に削減
- パフォーマンスの向上:不要な再レンダリングを抑制する設計
- 型安全性:Zod と TypeScript の連携により、強力な型推論が可能
- クリーンな実装:宣言的なバリデーションとフォーム管理
- エラーハンドリングの簡略化:エラーメッセージの管理が容易
<Form />
コンポーネントで Server Actions を使う方法
おまけ:ベータ版のReact Hook Form のベータ版では、新しい<Form />
コンポーネントが導入されています。
これは React 19 の Server Actions と直接統合できる新機能です!
主な特徴は以下の通りです:
- Server Actions との連携:React 19 の Server Actions と直接統合
- 自動化された FormData 処理:フォームの値を自動的に FormData に変換
- プログレッシブ・エンハンスメント:JavaScript が無効でも機能するフォーム
簡単な実装例を見てみましょう:
"use client";
import { startTransition, useActionState } from "react";
import { useForm, Form } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { newsletterSchema } from "./schemas";
export default function NewsletterForm() {
// Server Action の状態管理
const [state, formAction, isPending] = useActionState(subscribeToNewsletter, {
status: null,
message: "",
});
// React Hook Form の設定
const {
register,
control,
formState: { errors },
} = useForm({
resolver: zodResolver(newsletterSchema),
});
return (
<Form
control={control}
onSubmit={({ formData }) => startTransition(() => formAction(formData))}
>
<input id="email" {...register("email")} disabled={isPending} />
{errors.email && <p>{errors.email.message}</p>}
<button type="submit" disabled={isPending}>
{isPending ? "処理中..." : "購読する"}
</button>
{/* 成功・エラーメッセージ */}
</Form>
);
}
この例では:
- React 19 の
useActionState
を使って Server Action の状態を管理 - React Hook Form の
<Form>
コンポーネントを使用 -
control
プロパティを通じて RHF の機能を<Form>
に連携 -
onSubmit
で Server Action を実行(formData
が自動的に生成される)
これにより、Client Components と Server Components(RSC) を橋渡しする、
より宣言的なフォーム実装が可能になりますね!
おわりに
最後まで読んでいただき、ありがとうございます 🥳
ハンズオン形式で、
実際に手を動かしてフォーム実装を学習したい場合は、以下の教材もチェックしてみてください!!
この記事が、少しでも参考になれば嬉しいです!
そして、もし、間違いや補足情報などがありましたら、ぜひコメントを追加してください!
Happy Hacking :)
参考
Discussion