TypeScript × react-hook-form 入門 【第1回:基本実装編】
私はNext.js
とtailwindcss
を使用し、フロントエンド開発を行っている初心者です。フォーム実装において、バリデーションやエラーハンドリングに苦労していました。そこで、react-hook-form
の基本から応用まで、3回シリーズで解説していきたいと思います。
- 第1回:react-hook-formの基本実装(本記事)
- 第2回:zodによるバリデーション
- 第3回:react-hook-formとzodの連携
目次
はじめに
Reactアプリケーションでのフォーム開発は、状態管理やバリデーションの実装が複雑になりがちです。react-hook-form
は、この課題を解決するための強力なライブラリです。特にTypeScriptとの相性が良く、型安全性を保ちながら効率的なフォーム管理を実現できます。
本記事では、react-hook-form
の基本的な使い方に焦点を当て、実践的なコード例を交えながら解説します。
必要なパッケージ
npm
npm install react-hook-form
yarn
yarn add react-hook-form
基本的な使用例
フォームの書き方
シンプルなフォームを実装する前に、まずはreact-hook-form
でのフォームの書き方を簡単に解説します。通常のHTMLフォームと書き方の構成は同じです。入力フィールドは<div>
などで適切にグループ化します。
-
formタグでラップ
フォーム全体を<form>
タグで囲み、onSubmit
属性にhandleSubmit
メソッドを追加します。このメソッドはフォームが送信される際の処理です。 -
入力フィールドの登録
各入力フィールドはregister
メソッドを使用して登録します。これにより、フォームの入力を簡単に取得し、バリデーションを実施できます。 -
バリデーションの設定
バリデーションルールをregister
メソッドの第二引数としてオブジェクトで渡します。
以下は簡単な例です。
import { useForm } from 'react-hook-form';
// フォームデータの型定義
type FormInputs = {
username: string; // ユーザー名
email: string; // メールアドレス
};
const SimpleForm = () => {
// useFormフックでフォーム機能を使用
const { register, handleSubmit, formState: { errors } } = useForm<FormInputs>();
// フォーム送信時の処理
const onSubmit = (data: FormInputs) => {
console.log(data); // 入力データの出力
};
return (
// フォーム全体を<form>タグでラップ
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="username">ユーザー名</label>
<input
id="username" // フィールドのID
{...register("username", {
required: "ユーザー名は必須です" // 必須バリデーション
})}
/>
{/* エラーメッセージの表示(エラーがある場合のみ) */}
{errors.username && (
<span className="error">{errors.username.message}</span>
)}
</div>
<div>
<label htmlFor="email">メールアドレス</label>
<input
id="email" // フィールドのID
{...register("email", {
required: "メールアドレスは必須です", // 必須バリデーション
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i, // メールアドレスの形式チェック
message: "無効なメールアドレスです" // パターンが一致しない場合のエラーメッセージ
}
})}
/>
{/* エラーメッセージの表示(エラーがある場合のみ) */}
{errors.email && (
<span className="error">{errors.email.message}</span>
)}
</div>
<button type="submit">送信</button> // フォーム送信ボタン
</form>
);
};
コードの解説
このコード例の基本的な流れを解説します。
-
useFormフックの使用
-
useForm
フックは、フォームの主要な機能を提供します。このフックを呼び出すことで、register
,handleSubmit
,formState
などのメソッドやオブジェクトを取得できます。
-
-
型の定義
-
FormInputs
という型を定義し、フォームデータのプロパティ(ここではusername
とemail
)を指定しています。各フィールドの型を明確に指定することで入力値の安全性を確保しています。(TypeScriptの機能です)
-
-
フォーム送信時のデータ処理
-
handleSubmit
メソッドが呼ばれると、onSubmit
関数が実行されます。この関数内で、送信されたフォームデータがdata
として受け取られ、コンソールに出力されます。
-
-
入力フィールドの登録とバリデーション
- フォームの各入力フィールドに
register
メソッドを<input {...register("username")} />
のように、スプレッド構文で適用し、必須項目や正規表現パターンを使ったバリデーションルールを設定しています。 -
{...register("フィールド名")}
の形式で入力フィールドを登録し、フィールド名は必ず型定義で指定した名前と一致させる必要があります。 -
required
フィールドは、入力必須のバリデーションメッセージを設定します。 -
pattern
フィールドは、入力値が特定の正規表現に一致するか確認します。
- フォームの各入力フィールドに
-
エラーの表示
-
formState.errors
を使用して、バリデーションエラーが発生した場合に、対応するエラーメッセージを表示します。
-
主要な機能の解説
useFormフック
useForm
フックは、フォームの状態管理の中心となります。
const {
register, // 入力フィールドの登録
handleSubmit, // フォーム送信処理
formState, // フォームの状態
watch, // 値の監視
setValue, // 値の設定
reset, // フォームのリセット
control // フォーム全体の制御(入力値の変更を検知し、フォーム全体に反映)
formState: { // フォームの状態を詳細に管理
errors, // バリデーションエラー
isDirty, // フォームの値が変更されたかどうか
isValid, // フォームの全入力項目がバリデーション(入力規則)を満たしているか
isSubmitting // 送信中かどうか
}
} = useForm<FormInputs>();
registerメソッド
入力フィールドを登録し、バリデーションルールを設定します。
<input
{...register("fieldName", {
required: "必須項目です", // 必須項目(trueもしくはエラーメッセージの文字列を渡します)
minLength: { value: 4, message: "4文字以上必要です" }, // 最小文字数
maxLength: { value: 20, message: "20文字以内で入力してください" }, // 最大文字数
min: {value: 0, message: "0以上で入力してください"}, // 最小値
max: {value: 100, message: "100以下で入力してください"}, // 最大値
pattern: {value: /^[a-zA-Z]+$/, message: "英字で入力してください"}, // 正規表現によるパターンチェック
validate: (value) => value === "test" || "testと入力してください" // カスタムバリデーション
})}
/>
その他のルール
-
disabled
: 入力の無効化 -
valueAsNumber
: 数値として値を取得 -
valueAsDate
: 日付として値を取得
よく使用されるバリデーションルールの例
{
// 必須入力を設定(エラーメッセージ(文字列)か true/false(真偽値)で指定)
required: string | boolean,
// 数値の最小値を設定(数値型で指定)
min: number,
// 数値の最大値を設定(数値型で指定)
max: number,
// 文字列の最小文字数を設定(数値型で指定)
minLength: number,
// 文字列の最大文字数を設定(数値型で指定)
maxLength: number,
// 正規表現で入力パターンを設定(RegExp型: 文字列が特定のルールに従っているかチェックする型)
pattern: RegExp,
// カスタムルールで入力を検証(true(真)でOK、エラーメッセージ(文字列)を返す関数を指定)
validate: (value: any) => boolean | string
}
エラーハンドリング
formState.errors
を使用してエラーメッセージを表示
{/* errorsオブジェクトにfieldNameプロパティが存在し、かつそのmessageが存在する場合、エラーメッセージを表示する */}
{errors.fieldName && <span>{errors.fieldName.message}</span>}
パフォーマンスの最適化:不要な再レンダリングを防ぐ
フォームの実装では、入力値が変更されるたびに画面が再描画(再レンダリング)されることがあります。これが多すぎると、アプリケーションの動作が遅くなってしまいます。
react-hook-form
を使う際にも、特にフォームが大きくなるときに意識することで、より快適なユーザー体験を提供できます。
1. コンポーネントの分割による最適化
フォームを小さなコンポーネントに分けることで、必要な部分だけを更新できます。
フォームコンポーネント
const ParentForm = () => {
// フォーム全体の制御
const { control } = useForm<FormInputs>();
return (
// フォーム全体を囲む要素
<form>
{/* 子コンポーネントにフォームの制御情報を渡す */}
<UserNameField control={control} />
<EmailField control={control} />
</form>
);
};
子コンポーネント(ユーザー名入力用)
// ユーザー名入力フィールドが必要とするプロパティの型を定義
type UserNameFieldProps = {
control: Control<FormInputs>
};
const UserNameField = ({ control }: UserNameFieldProps) => {
// 入力フィールドのみを返す
return (
<Controller
// フィールドの名前を指定
name="username"
// フォームの制御情報を設定
control={control}
// 実際の入力要素をレンダリング
render={({ field }) => (
<input {...field} placeholder="ユーザー名" />
)}
/>
);
};
2. エラー表示の最適化
エラーメッセージの表示を別コンポーネントとして切り出すことで、エラー時の再レンダリングを制御できます。
エラーメッセージ用のコンポーネント
// エラーメッセージを表示するためのプロパティの型を定義
type ErrorMessageProps = {
message?: string
}
// エラーメッセージ用のコンポーネントを定義
const ErrorMessage = ({ message }: ErrorMessageProps) => {
// メッセージがある場合のみ表示
return message ? <span className="error">{message}</span> : null;
};
フォームコンポーネント
// フォームコンポーネントの定義
const SimpleForm = () => {
// フォームの機能を設定
const { register, formState: { errors } } = useForm<FormInputs>();
return (
// フォーム全体を囲む要素
<form>
{/* 入力フィールドを配置 */}
<input {...register("username")} />
{/* エラーメッセージコンポーネントを使用 */}
<ErrorMessage message={errors.username?.message} />
</form>
);
};
3. watch関数の最適化
フォームの値の監視は、必要な項目だけに限定することで、パフォーマンスを改善できます。
❌ 良くない例:すべてのフィールドを監視してしまう
const allFields = watch();
✅ 良い例:必要なフィールドだけを監視する
const username = watch("username");
// 値の変更を監視して処理を行う
useEffect(() => {
console.log(username); // 値が変更されたときだけ実行
}, [username]);
4. Controllerコンポーネントの活用
react-hook-form
のController
コンポーネントを使うと、より柔軟にフォームの制御ができます。
Controller
は、フォームの値を管理し、入力コンポーネントとの接続を簡単にする役割を持っています。(フォームフィールドをより簡単に管理)
通常の入力フォームと異なり、Controller
を使うと、再レンダリングを制御しやすくなります。
カスタム入力コンポーネント
// フォームの型定義
type FormInputs = {
myInput: string;
}
// フィールドプロップの型を定義
type FieldProps = {
name: string; // フィールドの名前
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void; // 値が変更された際のハンドラ
onBlur: () => void; // フィールドがフォーカスを失った際のハンドラ
value: string; // 現在のフィールドの値
};
// コンポーネントのProps型定義
type CustomInputProps = {
field: FieldProps;
placeholder: string;
};
// カスタム入力コンポーネントを定義
const CustomInput = ({ field, placeholder }: CustomInputProps) => {
// 入力フィールドを返す
return <input {...field} placeholder={placeholder} />;
};
フォームコンポーネント
// 親コンポーネントの定義
const ParentForm = () => {
// フォーム全体の制御
const { control } = useForm<FormInputs>();
return (
// フォーム全体を囲む要素
<form>
{/* Controllerを使ってカスタムコンポーネントと連携 */}
<Controller
// フォームに登録する名前
name="myInput"
// フォームの状態を管理
control={control}
// デフォルト値を設定
defaultValue=""
// バリデーションルールを設定
rules={{ required: true }}
// 入力要素をカスタマイズしてレンダリング
render={({ field, fieldState: { error } }) => (
<div>
{/* カスタム入力コンポーネントを使用 */}
<CustomInput field={field} placeholder="入力してください" />
{/* エラーメッセージを表示 */}
{error && <span>このフィールドは必須です</span>}
</div>
)}
/>
</form>
);
};
5. useMemoによる計算結果の再利用
値の再計算を防ぐことで、パフォーマンスを改善できます。useMemo
は、依存する値が変更されるまで、計算結果を再利用します。
特に、フォームの値に基づいて何かを計算する場合に有効です。
// useMemoフックを使うためのimport
import { useMemo } from 'react';
// フォームコンポーネントの定義
const MemoForm = () => {
// 値の監視
const { watch } = useForm<FormInputs>();
// usernameの値を取得
const username = watch("username");
// useMemoを使って、usernameが変更された時だけ計算
const formattedUsername = useMemo(() => {
// usernameを大文字に変換して返す
return username ? username.toUpperCase() : '';
}, [username]);
return (
// フォーム全体を囲む要素
<form>
{/* ユーザー名の入力欄 */}
<input {...register("username")} />
{/* 計算結果を表示 */}
<div>{formattedUsername}</div>
</form>
);
};
6. useCallbackによる関数のメモ化
関数の再生成を防ぐことで、パフォーマンスを改善できます。 useCallback
は、コンポーネントが再レンダリングされる度に新しい関数が作成されるのを防ぎます。
特に、子コンポーネントにpropsとして関数を渡す場合に有効です。(親コンポーネントが再レンダリングされるたびに、子コンポーネントの不要な再レンダリングを防げます)
子コンポーネント(住所検索ボタン)
// ボタンのProps型定義
type ButtonProps = {
onClick: () => void;
};
// 住所検索ボタンコンポーネント
const PostalSearchButton = ({ onClick }: ButtonProps) => {
return (
<button
onClick={onClick}
className="bg-blue-500 text-white px-4 py-2 rounded"
>
住所を検索
</button>
);
};
フォームコンポーネント
import { useCallback } from 'react';
// フォームの型定義
type FormInputs = {
postalCode: string;
address: string;
};
// 配送先入力フォーム
const DeliveryForm = () => {
const { register, setValue } = useForm<FormInputs>();
// 郵便番号から住所を自動入力
// 実際のアプリケーションでは、ここで郵便番号検索APIを呼び出します
// 例: https://zipcloud.ibsnet.co.jp/api/search?zipcode=1234567
const handlePostalSearch = useCallback(async () => {
// 郵便番号検索APIを呼び出し
const response = await fetch(
// 省略
}
// APIから返ってきたデータをJSONに変換
const data = await response.json();
// APIからの返却値を住所として結合
const address = `${data.results[0].address1}${data.results[0].address2}${data.results[0].address3}`;
setValue('address', address);
}, [setValue]);
return (
// フォーム全体を囲む要素
<form className="space-y-4">
// 郵便番号入力欄と検索ボタン
<div className="flex items-center gap-2">
// 郵便番号を入力するテキストボックス
<input {...register('postalCode')} placeholder="郵便番号" />
// 住所検索を実行するボタン
<PostalSearchButton onClick={handlePostalSearch} />
</div>
// 住所入力欄
<div>
// 住所を入力するテキストボックス
<input {...register('address')} placeholder="住所" className="w-full" />
</div>
</form>
);
};
これらの最適化は、フォームが大きくなってきた時や、動作が遅くなってきた時に適用することをおすすめします。小規模なフォームでは、必ずしも必要ではありません。
トラブルシューティング
よくあるエラーと解決方法
-
型定義エラー
// 解決策:明示的な型定義 type FormData = { username: string; email: string; age: number; isSubscribed: boolean; };
-
デフォルト値の設定
// useFormフックからregister関数を取得し、フォームデータの型をFormDataに設定、初期値をusernameとemailを空文字で設定 const { register } = useForm<FormData>({ defaultValues: { username: '', email: '' } });
実装時の注意点
- フォームフィールドの名前は型定義と一致させる
- バリデーションメッセージは必ず設定する
- エラー表示のUIは適切に実装する
発展的な使用例
フォームの監視
const watchUsername = watch("username");
useEffect(() => {
console.log(watchUsername); // 値が変更されるたびに実行
}, [watchUsername]);
動的なフォーム
// useFieldArray:フォーム内で動的に増減できる配列型の入力欄を管理するためのフック
// useFieldArrayフックを使用して、"items"という名前のフィールド配列を制御
// 配列のフィールドを取得、追加、削除するための関数を定義する。
const { fields, append, remove } = useFieldArray({
control,
name: "items"
});
まとめ
本記事では、react-hook-form
の基本的な使い方について解説しました。シンプルな実装で強力なフォーム管理が可能となり、特にTypeScriptとの親和性の高さが特徴です。次回はzod
によるバリデーションについて解説し、最終回でreact-hook-form
とzod
の連携方法を紹介する予定です。
もし記事の内容に間違いや改善点がありましたら、コメントでご指摘いただけますと幸いです。また、実務での活用事例やベストプラクティスについても、ぜひ共有していただければと思います。
次回の記事では、zod
を使用したより堅牢なバリデーション実装について解説していきますので、ご期待ください!