はじめに#
Reactでフォームを実装する際、バリデーションロジックと型定義の管理は開発者にとって大きな課題です。React Hook FormとZodを組み合わせることで、型安全なフォームバリデーションを効率的に実装できます。
React Hook Formは、高パフォーマンスで柔軟なフォーム管理ライブラリです。一方、ZodはTypeScriptファーストのバリデーションライブラリで、スキーマ定義から自動的に型を生成できます。この2つを連携させることで、バリデーションルールと型定義を一元管理し、開発時の型補完と実行時の安全性を両立できます。
この記事を読み終えると、以下のことができるようになります。
- React Hook FormとZodの連携セットアップを完了できる
- 型安全なフォームバリデーションを実装できる
- カスタムエラーメッセージを設定できる
- 動的フォーム(配列フィールド)を型安全に実装できる
- 実務で活用できるフォームパターンを理解できる
実行環境と前提条件#
前提知識#
- Reactの基本(コンポーネント、フック)
- TypeScriptの基本的な型注釈
- フォーム要素の基本的な使い方
動作確認環境#
| ツール |
バージョン |
| Node.js |
20.x以上 |
| React |
18.x以上 |
| TypeScript |
5.5以上 |
| React Hook Form |
7.55以上 |
| Zod |
4.x |
| @hookform/resolvers |
5.x以上 |
期待される結果#
本記事のコードを実行すると、以下の動作を確認できます。
- Zodスキーマに基づいたフォームバリデーションが動作する
- TypeScriptによる型補完がフォームデータに適用される
- バリデーションエラーが適切に表示される
- 動的フォームの追加・削除が型安全に動作する
React Hook Formは、Reactでフォームを構築するためのライブラリです。非制御コンポーネントベースのアプローチにより、再レンダリングを最小限に抑え、高いパフォーマンスを実現します。
主な特徴は以下のとおりです。
- 軽量で依存関係がない
- 再レンダリングの最小化によるパフォーマンス向上
- HTML標準のバリデーションAPIとの互換性
- 外部バリデーションライブラリとの統合が容易
Zodとは#
Zodは、TypeScriptファーストのスキーマバリデーションライブラリです。スキーマ定義から自動的にTypeScript型を生成できるため、バリデーションロジックと型定義の二重管理を回避できます。
主な特徴は以下のとおりです。
- 外部依存なしの軽量設計(コアバンドル2KB gzipped)
- スキーマからの型推論(
z.infer)
- 豊富なバリデーションメソッド
- カスタムエラーメッセージのサポート
なぜ組み合わせるのか#
React Hook Form単体でもバリデーションは可能ですが、Zodと組み合わせることで以下のメリットが得られます。
graph LR
A[Zodスキーマ] --> B[型定義の自動生成]
A --> C[バリデーションルール]
B --> D[TypeScript型補完]
C --> E[実行時バリデーション]
D --> F[開発時の安全性]
E --> F
- 型とバリデーションの一元管理: スキーマを1か所で定義するだけで、型定義とバリデーションルールの両方が得られます
- 再利用性の向上: 同じスキーマをAPI通信やデータ検証など、フォーム以外でも再利用できます
- 保守性の向上: バリデーションルールの変更が自動的に型に反映されます
セットアップ手順#
パッケージのインストール#
React Hook Form、Zod、およびresolverパッケージをインストールします。
1
|
npm install react-hook-form zod @hookform/resolvers
|
yarnを使用する場合は以下のコマンドを実行します。
1
|
yarn add react-hook-form zod @hookform/resolvers
|
pnpmを使用する場合は以下のコマンドを実行します。
1
|
pnpm add react-hook-form zod @hookform/resolvers
|
TypeScript設定の確認#
Zodを使用するには、tsconfig.jsonでstrictモードを有効にする必要があります。
1
2
3
4
5
6
7
8
9
10
|
{
"compilerOptions": {
"strict": true,
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "bundler"
}
}
|
基本的なフォーム実装例#
シンプルなログインフォーム#
React Hook FormとZodを使った基本的なフォーム実装を見ていきましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
|
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
// 1. Zodスキーマを定義
const loginSchema = z.object({
email: z
.string()
.min(1, "メールアドレスを入力してください")
.email("有効なメールアドレスを入力してください"),
password: z
.string()
.min(1, "パスワードを入力してください")
.min(8, "パスワードは8文字以上で入力してください"),
});
// 2. スキーマから型を生成
type LoginFormData = z.infer<typeof loginSchema>;
// 3. フォームコンポーネント
export const LoginForm = () => {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
});
const onSubmit = async (data: LoginFormData) => {
// data は LoginFormData 型として型安全に利用可能
console.log("送信データ:", data);
// API呼び出しなどの処理
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="email">メールアドレス</label>
<input
id="email"
type="email"
{...register("email")}
aria-invalid={errors.email ? "true" : "false"}
/>
{errors.email && (
<p role="alert">{errors.email.message}</p>
)}
</div>
<div>
<label htmlFor="password">パスワード</label>
<input
id="password"
type="password"
{...register("password")}
aria-invalid={errors.password ? "true" : "false"}
/>
{errors.password && (
<p role="alert">{errors.password.message}</p>
)}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "送信中..." : "ログイン"}
</button>
</form>
);
};
|
実装のポイント#
上記の実装には、いくつかの重要なポイントがあります。
zodResolverの使用
@hookform/resolversパッケージのzodResolverを使用することで、ZodスキーマをReact Hook Formのバリデーションに統合できます。
1
2
3
|
const { register, handleSubmit } = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
});
|
型推論の活用
z.infer<typeof schema>を使用すると、スキーマ定義から自動的にTypeScript型が生成されます。これにより、フォームデータに対して完全な型補完が有効になります。
1
2
3
4
5
6
|
type LoginFormData = z.infer<typeof loginSchema>;
// 生成される型:
// {
// email: string;
// password: string;
// }
|
エラーハンドリング
formState.errorsオブジェクトから各フィールドのエラーメッセージを取得できます。Zodで定義したカスタムメッセージがそのまま表示されます。
ユーザー登録フォームの実装#
より実践的な例として、ユーザー登録フォームを実装します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
|
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
// パスワード確認を含むスキーマ
const registerSchema = z
.object({
username: z
.string()
.min(1, "ユーザー名を入力してください")
.min(3, "ユーザー名は3文字以上で入力してください")
.max(20, "ユーザー名は20文字以下で入力してください")
.regex(
/^[a-zA-Z0-9_]+$/,
"ユーザー名は英数字とアンダースコアのみ使用できます"
),
email: z
.string()
.min(1, "メールアドレスを入力してください")
.email("有効なメールアドレスを入力してください"),
password: z
.string()
.min(1, "パスワードを入力してください")
.min(8, "パスワードは8文字以上で入力してください")
.regex(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
"パスワードは大文字、小文字、数字を含める必要があります"
),
confirmPassword: z.string().min(1, "パスワード確認を入力してください"),
age: z
.number({ invalid_type_error: "年齢は数値で入力してください" })
.min(18, "18歳以上である必要があります")
.max(120, "有効な年齢を入力してください"),
acceptTerms: z.literal(true, {
errorMap: () => ({ message: "利用規約に同意してください" }),
}),
})
.refine((data) => data.password === data.confirmPassword, {
message: "パスワードが一致しません",
path: ["confirmPassword"],
});
type RegisterFormData = z.infer<typeof registerSchema>;
export const RegisterForm = () => {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<RegisterFormData>({
resolver: zodResolver(registerSchema),
defaultValues: {
username: "",
email: "",
password: "",
confirmPassword: "",
acceptTerms: false,
},
});
const onSubmit = async (data: RegisterFormData) => {
console.log("登録データ:", data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="username">ユーザー名</label>
<input
id="username"
type="text"
{...register("username")}
/>
{errors.username && <p>{errors.username.message}</p>}
</div>
<div>
<label htmlFor="email">メールアドレス</label>
<input
id="email"
type="email"
{...register("email")}
/>
{errors.email && <p>{errors.email.message}</p>}
</div>
<div>
<label htmlFor="password">パスワード</label>
<input
id="password"
type="password"
{...register("password")}
/>
{errors.password && <p>{errors.password.message}</p>}
</div>
<div>
<label htmlFor="confirmPassword">パスワード確認</label>
<input
id="confirmPassword"
type="password"
{...register("confirmPassword")}
/>
{errors.confirmPassword && (
<p>{errors.confirmPassword.message}</p>
)}
</div>
<div>
<label htmlFor="age">年齢</label>
<input
id="age"
type="number"
{...register("age", { valueAsNumber: true })}
/>
{errors.age && <p>{errors.age.message}</p>}
</div>
<div>
<label>
<input
type="checkbox"
{...register("acceptTerms")}
/>
利用規約に同意する
</label>
{errors.acceptTerms && <p>{errors.acceptTerms.message}</p>}
</div>
<button type="submit" disabled={isSubmitting}>
登録
</button>
</form>
);
};
|
refineによるクロスフィールドバリデーション#
refineメソッドを使用すると、複数フィールドにまたがるバリデーションを実装できます。パスワード確認のように、2つのフィールドの値を比較する場合に有効です。
1
2
3
4
5
6
7
8
9
|
const schema = z
.object({
password: z.string(),
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "パスワードが一致しません",
path: ["confirmPassword"], // エラーを表示するフィールドを指定
});
|
数値フィールドの扱い#
HTMLのinput[type="number"]は文字列を返すため、valueAsNumberオプションを使用して数値に変換します。
1
2
3
4
|
<input
type="number"
{...register("age", { valueAsNumber: true })}
/>
|
エラーメッセージのカスタマイズ#
基本的なカスタマイズ#
Zodでは、各バリデーションメソッドにカスタムエラーメッセージを渡せます。
1
2
3
4
5
6
7
8
9
10
|
const schema = z.object({
email: z
.string()
.min(1, "メールアドレスは必須です")
.email("メールアドレスの形式が正しくありません"),
password: z
.string()
.min(1, "パスワードは必須です")
.min(8, "パスワードは8文字以上必要です"),
});
|
errorMapによる一括カスタマイズ#
errorMapを使用すると、エラーメッセージをより詳細にカスタマイズできます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
const schema = z.object({
age: z.number({
required_error: "年齢を入力してください",
invalid_type_error: "年齢は数値で入力してください",
}),
status: z.enum(["active", "inactive"], {
errorMap: (issue, ctx) => {
if (issue.code === "invalid_enum_value") {
return { message: "有効なステータスを選択してください" };
}
return { message: ctx.defaultError };
},
}),
});
|
条件付きエラーメッセージ#
superRefineを使用すると、複雑な条件に基づいたエラーメッセージを生成できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
const passwordSchema = z.string().superRefine((val, ctx) => {
if (val.length < 8) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "パスワードは8文字以上必要です",
});
}
if (!/[A-Z]/.test(val)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "大文字を1文字以上含めてください",
});
}
if (!/[a-z]/.test(val)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "小文字を1文字以上含めてください",
});
}
if (!/\d/.test(val)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "数字を1文字以上含めてください",
});
}
});
|
すべてのエラーを表示する#
デフォルトでは、各フィールドの最初のエラーのみが表示されます。すべてのエラーを表示するには、criteriaModeをallに設定します。
1
2
3
4
|
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema),
criteriaMode: "all",
});
|
動的フォームの実装例#
useFieldArrayによる配列フィールド#
複数の入力フィールドを動的に追加・削除するフォームを実装します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
|
import { useForm, useFieldArray } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
// スキルを複数登録できるスキーマ
const profileSchema = z.object({
name: z.string().min(1, "名前を入力してください"),
skills: z
.array(
z.object({
name: z.string().min(1, "スキル名を入力してください"),
level: z.enum(["beginner", "intermediate", "advanced"], {
errorMap: () => ({ message: "レベルを選択してください" }),
}),
})
)
.min(1, "スキルを1つ以上追加してください"),
});
type ProfileFormData = z.infer<typeof profileSchema>;
export const ProfileForm = () => {
const {
register,
control,
handleSubmit,
formState: { errors },
} = useForm<ProfileFormData>({
resolver: zodResolver(profileSchema),
defaultValues: {
name: "",
skills: [{ name: "", level: "beginner" }],
},
});
const { fields, append, remove } = useFieldArray({
control,
name: "skills",
});
const onSubmit = (data: ProfileFormData) => {
console.log("プロフィールデータ:", data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="name">名前</label>
<input id="name" {...register("name")} />
{errors.name && <p>{errors.name.message}</p>}
</div>
<fieldset>
<legend>スキル</legend>
{errors.skills?.root && (
<p>{errors.skills.root.message}</p>
)}
{fields.map((field, index) => (
<div key={field.id}>
<div>
<label htmlFor={`skills.${index}.name`}>
スキル名
</label>
<input
id={`skills.${index}.name`}
{...register(`skills.${index}.name`)}
/>
{errors.skills?.[index]?.name && (
<p>{errors.skills[index].name.message}</p>
)}
</div>
<div>
<label htmlFor={`skills.${index}.level`}>
レベル
</label>
<select
id={`skills.${index}.level`}
{...register(`skills.${index}.level`)}
>
<option value="">選択してください</option>
<option value="beginner">初級</option>
<option value="intermediate">中級</option>
<option value="advanced">上級</option>
</select>
{errors.skills?.[index]?.level && (
<p>{errors.skills[index].level.message}</p>
)}
</div>
<button
type="button"
onClick={() => remove(index)}
disabled={fields.length === 1}
>
削除
</button>
</div>
))}
<button
type="button"
onClick={() => append({ name: "", level: "beginner" })}
>
スキルを追加
</button>
</fieldset>
<button type="submit">保存</button>
</form>
);
};
|
useFieldArrayの動作#
sequenceDiagram
participant User as ユーザー
participant Form as フォーム
participant Array as useFieldArray
participant Zod as Zodスキーマ
User->>Form: 「スキルを追加」クリック
Form->>Array: append({ name: "", level: "beginner" })
Array->>Form: fields配列を更新
Form->>User: 新しい入力フィールドを表示
User->>Form: フォーム送信
Form->>Zod: バリデーション実行
Zod-->>Form: バリデーション結果
Form->>User: エラー表示 or 送信完了useFieldArrayを使用する際のポイントは以下のとおりです。
controlオブジェクトを渡してフォームとの連携を確立する
fields配列の各要素には一意のidプロパティが自動付与される
appendで追加、removeで削除、updateで更新ができる
- Zodスキーマで配列全体と各要素のバリデーションを定義する
実務での活用パターン#
パターン1: フォームコンポーネントの分離#
大規模なフォームでは、入力フィールドをコンポーネントとして分離すると保守性が向上します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
import { useFormContext } from "react-hook-form";
// 再利用可能な入力コンポーネント
type TextFieldProps = {
name: string;
label: string;
type?: "text" | "email" | "password";
};
export const TextField = ({ name, label, type = "text" }: TextFieldProps) => {
const {
register,
formState: { errors },
} = useFormContext();
const error = errors[name];
return (
<div>
<label htmlFor={name}>{label}</label>
<input
id={name}
type={type}
{...register(name)}
aria-invalid={error ? "true" : "false"}
/>
{error && <p role="alert">{error.message as string}</p>}
</div>
);
};
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
import { FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { TextField } from "./TextField";
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
type FormData = z.infer<typeof schema>;
export const LoginFormWithProvider = () => {
const methods = useForm<FormData>({
resolver: zodResolver(schema),
});
const onSubmit = (data: FormData) => {
console.log(data);
};
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<TextField name="email" label="メールアドレス" type="email" />
<TextField name="password" label="パスワード" type="password" />
<button type="submit">送信</button>
</form>
</FormProvider>
);
};
|
パターン2: スキーマの再利用#
バックエンドとフロントエンドでスキーマを共有することで、型の一貫性を保てます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
// schemas/user.ts
import { z } from "zod";
// 基本スキーマ
export const userBaseSchema = z.object({
email: z.string().email("有効なメールアドレスを入力してください"),
name: z.string().min(1, "名前を入力してください"),
});
// 登録用スキーマ(パスワード必須)
export const userRegistrationSchema = userBaseSchema.extend({
password: z.string().min(8, "パスワードは8文字以上必要です"),
});
// 更新用スキーマ(パスワード任意)
export const userUpdateSchema = userBaseSchema.extend({
password: z.string().min(8).optional(),
});
// API レスポンス用スキーマ
export const userResponseSchema = userBaseSchema.extend({
id: z.number(),
createdAt: z.string().datetime(),
});
// 型のエクスポート
export type UserRegistration = z.infer<typeof userRegistrationSchema>;
export type UserUpdate = z.infer<typeof userUpdateSchema>;
export type UserResponse = z.infer<typeof userResponseSchema>;
|
パターン3: 非同期バリデーション#
メールアドレスの重複チェックなど、サーバーへの問い合わせが必要なバリデーションを実装します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const checkEmailExists = async (email: string): Promise<boolean> => {
const response = await fetch(`/api/check-email?email=${email}`);
const data = await response.json();
return data.exists;
};
const schema = z.object({
email: z
.string()
.email("有効なメールアドレスを入力してください")
.refine(
async (email) => {
const exists = await checkEmailExists(email);
return !exists;
},
{ message: "このメールアドレスは既に登録されています" }
),
password: z.string().min(8),
});
type FormData = z.infer<typeof schema>;
export const AsyncValidationForm = () => {
const {
register,
handleSubmit,
formState: { errors, isValidating },
} = useForm<FormData>({
resolver: zodResolver(schema),
mode: "onBlur", // フォーカスが外れたときにバリデーション
});
const onSubmit = (data: FormData) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="email">メールアドレス</label>
<input id="email" type="email" {...register("email")} />
{isValidating && <span>確認中...</span>}
{errors.email && <p>{errors.email.message}</p>}
</div>
<div>
<label htmlFor="password">パスワード</label>
<input
id="password"
type="password"
{...register("password")}
/>
{errors.password && <p>{errors.password.message}</p>}
</div>
<button type="submit" disabled={isValidating}>
登録
</button>
</form>
);
};
|
パターン4: 条件付きフィールド#
選択内容に応じて表示・バリデーションするフィールドを切り替えます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
import { useForm, useWatch } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const schema = z.discriminatedUnion("contactMethod", [
z.object({
contactMethod: z.literal("email"),
email: z.string().email("有効なメールアドレスを入力してください"),
}),
z.object({
contactMethod: z.literal("phone"),
phone: z.string().regex(/^0\d{9,10}$/, "有効な電話番号を入力してください"),
}),
]);
type FormData = z.infer<typeof schema>;
export const ConditionalForm = () => {
const {
register,
control,
handleSubmit,
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
contactMethod: "email",
},
});
const contactMethod = useWatch({ control, name: "contactMethod" });
const onSubmit = (data: FormData) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>連絡方法</label>
<select {...register("contactMethod")}>
<option value="email">メール</option>
<option value="phone">電話</option>
</select>
</div>
{contactMethod === "email" && (
<div>
<label htmlFor="email">メールアドレス</label>
<input id="email" type="email" {...register("email")} />
{errors.email && <p>{errors.email.message}</p>}
</div>
)}
{contactMethod === "phone" && (
<div>
<label htmlFor="phone">電話番号</label>
<input id="phone" type="tel" {...register("phone")} />
{errors.phone && <p>{errors.phone.message}</p>}
</div>
)}
<button type="submit">送信</button>
</form>
);
};
|
まとめ#
React Hook FormとZodを組み合わせることで、以下のメリットを得られます。
- 型安全性: Zodスキーマから自動生成される型により、コンパイル時にエラーを検出できます
- 一元管理: バリデーションルールと型定義を1か所で管理でき、保守性が向上します
- パフォーマンス: React Hook Formの非制御コンポーネントアプローチにより、再レンダリングが最小限に抑えられます
- 再利用性: スキーマをAPI通信やデータ検証など、フォーム以外でも再利用できます
実務では、フォームコンポーネントの分離、スキーマの共有、非同期バリデーション、条件付きフィールドなど、様々なパターンを組み合わせて使用することになります。本記事で紹介したパターンを参考に、プロジェクトに適した実装を検討してください。
参考リンク#