【ITニュース解説】The Ultimate React Hook Form + Zod Pattern for Reusable Create and Edit Forms
2025年09月25日に「Dev.to」が公開したITニュース「The Ultimate React Hook Form + Zod Pattern for Reusable Create and Edit Forms」について初心者にもわかりやすく解説しています。
ITニュース概要
Reactのフォームでは、新規作成と編集で似たようなコードが重複し、管理が大変になる問題がある。この問題を解決するため、Zodスキーマとデフォルト値をカスタムフックに集約する。これにより、コードの重複をなくし、一つのフォームで両機能に対応できるため、開発効率と保守性が向上する。
ITニュース解説
Reactアプリケーションでウェブフォームを実装する際、特にデータの新規作成と既存データの編集の両方に対応するフォームを作成するとき、開発者はコードの重複という共通の課題に直面する。フォームの入力検証ルールや初期値の設定は複雑になりがちで、新規作成用と編集用でほとんど同じようなコードを何度も書く必要が生じる。このような重複したコードは、後でフォームの要件が変更された場合に、複数のファイルや箇所を修正しなければならないため、保守性が著しく低下し、修正漏れによるバグの原因にもなりやすい。結果として、開発者は検証と初期値の管理に多くの労力を費やし、開発効率が落ちることが問題であった。
この問題を効果的に解決するための一つのパターンとして、フォームの入力検証スキーマとデフォルト値を一つのカスタムフックにまとめる方法が提案されている。このアプローチでは、Zodのようなスキーマ定義ライブラリを使用してフォームの入力項目に対する厳密な検証ルール(例えば、特定のフィールドが必須であるか、数値の範囲、文字列の長さなど)を定義し、さらにフォームの初期値を設定するロジックもこのフック内に含める。この統一された管理方法により、新規作成フォームと編集フォームの両方で同じカスタムフックを再利用できるようになり、コードの重複を排除し、保守性を大幅に向上させることが可能となる。
具体的には、useProfileSchemaという名前のカスタムフックを作成する。このフックは、フォームに表示するデータ(例:編集対象の既存ユーザー情報)をdataという引数として受け取る。このdataが渡されなかった場合は、新しいデータを作成するためのフォームとみなし、空の文字列や初期の数値(例えば年齢なら18)といった、事前に定義されたデフォルト値を生成する。一方、dataが渡された場合は、その既存のデータを使って編集フォームの初期値を設定する。フックの内部では、z.objectというZodの機能を使って、ユーザー名が必須であること、年齢が18歳以上であること、購読設定が真偽値であることなど、各入力フィールドに対する具体的な検証ルールを記述したスキーマを定義する。同時に、dataの有無に応じて、usernameやage、subscribeなどの初期値を決定するデフォルト値オブジェクトも生成する。そして、このカスタムフックは、定義されたスキーマと生成されたデフォルト値を一つのオブジェクトとして呼び出し元に返す。useMemoというReactのフックを使うことで、これらの値が不必要に再計算されることを防ぎ、パフォーマンスを最適化する。
このカスタムフックを実際のフォームコンポーネントで利用する際は、まずReact Hook Formというライブラリが提供するuseFormフックを呼び出す。このuseFormフックには、先ほど作成したカスタムフックから取得したスキーマをzodResolver(ZodスキーマをReact Hook Formで使うためのアダプター)を通してresolverオプションに渡し、さらにカスタムフックから取得したデフォルト値をdefaultValuesオプションに設定する。これにより、フォームの入力検証と初期値の設定が非常に簡潔に、かつ連携して行えるようになる。フォームの送信処理はform.handleSubmitに渡すだけでよく、各入力フィールドはform.registerという機能を使ってReact Hook Formに登録する。formState.errorsオブジェクトを参照すれば、検証ルールに反した場合のエラーメッセージをユーザーインターフェースに表示することも容易である。また、フォームを送信するボタンのテキストも、initialData.id(編集対象のデータが存在するかどうか)の有無によって「更新」と「作成」を自動的に切り替えるなど、柔軟なUI表現が可能となる。この一連の仕組みにより、一つのフォームコンポーネントが新規作成と編集の両方の役割を果たすことができる。
このパターンをプロジェクトに導入することで、開発者は複数のメリットを得られる。最も重要なのは、「単一の真実の源」が確立されることである。検証ルールとデフォルト値がカスタムフックという一箇所に集約されるため、常に同期された状態が保たれる。これにより、コードの重複が解消され(DRY原則に従う)、メンテナンスが格段に容易になる。もしフォームの検証ルールや初期値のロジックに変更が生じても、修正が必要なのはカスタムフック内の特定の箇所一ヶ所で済むため、変更漏れによるバグの発生リスクを低減できる。さらに、コードの構成がより明確になり、新しくプロジェクトに参加したチームメンバーでもフォーム関連のロジックを素早く理解し、スムーズに開発に参加できるという利点がある。
このパターンは、さらに発展させて活用することも可能だ。一つはTypeScriptを導入し、フォームデータの型を厳密に定義することで、開発時の安全性とコード補完の精度を向上させる方法である。フォームデータの型をProfileFormDataのように明示的に定義し、カスタムフックの引数や戻り値に適用することで、より堅牢でエラーに強いコードベースを構築できる。
また、バックエンドAPIから取得するデータ形式と、フロントエンドのフォームで扱うデータ形式が異なる場合に備え、データ変換関数をカスタムフックに含めることも有効だ。例えば、APIがuser_nameというフィールド名を使用しているが、フォームではusernameとして扱いたい場合、apiToForm関数でAPIデータをフォーム形式に変換し、逆にフォームからAPIへデータを送信する際にはformToApi関数でフォームデータをAPI形式に変換するといった処理をフック内に含めることができる。これにより、データの整合性を保ちつつ、APIとフォーム間のマッピングを一元的に管理できる。
さらに複雑なフォームでは、特定の入力フィールドの値に基づいて検証ルールが変わる「条件付きバリデーション」が必要となる場合がある。Zodのrefineメソッドを使用すれば、例えば支払い方法がクレジットカードの場合にカード番号が必須となり、銀行口座の場合に口座番号が必須となる、といった複雑な検証ロジックもカスタムフック内で記述できる。
フォームのリセット機能も開発において重要である。useEffectフックを利用し、フォームのinitialData(初期表示データ)が変更されたときにform.reset(defaults)を呼び出すことで、編集フォームでデータを更新した後や、異なるデータをロードした際にフォームの値を適切にリセットできる。また、ユーザーが手動でフォームの入力内容を初期状態に戻したい場合に備え、「リセット」ボタンにform.reset(defaults)の呼び出しを紐付けることも可能である。
大規模なフォームでパフォーマンスが懸念される場合は、useMemoをさらに活用することで最適化を図れる。カスタムフックの引数として渡されるdataオブジェクト全体を依存配列に含めるのではなく、その中の特定のキー(例:username、age、subscribeなど)のみをuseMemoの依存配列に指定することで、不必要な再計算を抑制し、フォームのレンダリングパフォーマンスを向上させることができる。
このカスタムフックを使ったパターンは、React Hook Formだけでなく、他のReactフォームライブラリにも応用可能な、普遍的な設計原則に基づいている。フォームの状態管理や検証ロジックの複雑さに悩まされてきた開発者にとって、このアプローチは開発効率とコードの品質を大きく向上させる強力な解決策となる。フォーム実装の煩雑さを解消し、よりスムーズで効率的な開発体験をもたらすことができる。