700行の動的フォームをリファクタリングした
2026-03-02
azblob://2026/03/02/eyecatch/2026-02-27-refactor-adaptive-form-000.png

対象読者

Reactで動的フォームの実装・保守に携わっている方や、コンポーネント設計やリファクタリングに関心のある方に向けて書いています。特に「なんとなく動いているけどコードが読みにくい…」と感じている方の参考になれば嬉しいです。

話さないこと

  • 具体的なReact Hook Formの使い方(基本的な使い方の記事はこちら
  • ディレクトリ構成のベストプラクティス
  • Reactのベストプラクティス

はじめに

今回扱ったフォームは、APIレスポンスによって設問内容が決まる構造になっています。フォームの構造(各設問の文言や入力UIのタイプ、必須/任意、選択肢)はフォーム取得APIから取得しました。一部の設問には「Q1で『あり』を選択した場合のみQ2を表示する」といった表示条件があり、回答内容に応じて表示される設問が切り替わるため、フロントエンド側では必須チェックバリデーション・表示制御・入力状態管理・送信ロジックを組み合わせて実装する必要がありました。

また、このフォームは以下のように「グループ」単位で設問を表示する構造になっており、「次へ」を押すと問診全体のレスポンスを更新しながら次のグループへ遷移します。フォーム送信処理とページ切り替え処理が密接に関係する仕様でした。


// 擬似コード(JSONC: コメント付きのJSON風表記)
{
  "response": [
    {
      "groupTitle": "○○について",
      "childItems": [
        {
          "question": "○○ですか",
          "inputType": "text", // テキスト入力やセレクトボックスなど
          "isRequired": true,
          "option": [] // セレクトボックスなどの選択肢
        },
        {
          // 他の設問
        }
      ]
    },
    {
      // 他のグループ
    }
  ]
}

リファクタリング前の状況

リファクタリング前の実装は約700行の単一コンポーネントで、React Hook FormとuseStateが混在し、入力UIも分離されていない状態でした。機能追加や修正のたびに条件分岐や副作用が増え、影響範囲を把握するのに時間がかかる構造になっていました。 
変数や関数を追うだけでもページの先頭に飛んだり、中間のロジックを見るためにページの中央あたりを見たりと、認知負荷がとても大きかったです。

感じた課題

動的フォームは、一言で言えば「レスポンスの構造をフロントエンドに落とし込む」ことで実装できます。ただ、一見シンプルに見えても実際の実装となると、表示制御・状態管理・遷移制御といった責務が自然と集約されやすく、構造を整理しないまま機能追加を重ねると、急速に肥大化していきます。

今回の課題はまさに、責務が混在したまま積み上がり、肥大化していたことでした。

本記事では、この700行の動的フォームをどのように整理し、責務を分離していったのか、その過程と学びをまとめます。単なる行数削減ではなく、「変更しやすい構造」を目指したリファクタリングの話をしていきます!

ワイヤーフレーム起点で考えた分離方針

まず最初に、コンポーネント分離の方針はワイヤーフレームを起点に設計しました。ワイヤーフレームは最低限の要素だけを配置し、まずはその要素を分離する方針で考えはじめた。そこから「各コンポーネントにどのロジックを置くか」を順に決めていきました。 
このときの主な決定は次のとおりです。

  • Page(外枠):進捗/画面遷移と、問診全体のレスポンス(APIに送る塊)の高レベル管理を担当し、個々の入力値やバリデーションの詳細は持たせない。
  • Form(中央):現在のグループのレンダリング、入力値の集約、グループ単位の保存処理を担う中核。状態は一元化(RHF等)して Form が管理する。
  • 入力UI(小さな部品):値表示・入力・エラー表示を担保、バリデーションの判定は原則「スキーマ(Zod等)+RHF resolver」で行う。業務ロジックや表示条件の決定はできるだけ上位に委譲する。
  • データ変換/スキーマ(独立レイヤー):APIとフォーム値の変換、及び Zod 等のバリデーションスキーマは別ファイルに置くことで、変換ルールと UI 実装を切り離した。

このワイヤーフレーム起点の考え方により、「どのロジックをどのコンポーネントに置けば影響範囲が小さく保守しやすくなるか」が明確になり、実際の分割・実装(プロトタイプ検証)へスムーズにつなげられました。

ワイヤーフレーム

図:ワイヤーフレーム

プロトタイプ検証について

余談ですが、分離方針を固めると同時に React Hook Form の挙動確認も兼ねて短いプロトタイプを作りました。そのおかげで 2日で方針を最終決定 → 続けて3日で本実装のリファクタリングを完了 できました。プロトタイプがなければ検証に5日以上かかっていたか、あるいはリファクタリング自体を諦めていた可能性が高く、短期間のプロトタイプが方針決定の速度と実装リスクの低減にすごく効いた、というのが実感です。

また、今回のプロトタイプ検証は生成AIの活用という観点でも有効だったと感じています。検証の過程で「どんなプロンプトで、どの粒度のコンテキストを渡せば安定して活用できるのか」を試行錯誤できました。うまくいかないケースもありましたが、その都度いったんリセットし、コンテキストや指示の出し方を調整し直すことができました。

その結果、本実装フェーズでは生成AIの使い方がある程度固まっており、設計確認やコード生成の精度が安定しました。そういった準備があったからこそ、リファクタリングを3日で完了できた側面も大きいと感じています。

実装とその後の経過

プロトタイプで得た知見を反映して一度本実装を行い、ノウハウを蓄積しました(残念ながらその実装は使われなくなってしまいましたが...泣)。しかし、そこで得た設計上の知見を、次の実装に活かすことができました。最終的にはワイヤーフレーム起点での責務分割を踏まえた構造で整理・実装でき、結果として API の差し替えの変更を最小限のコード差分で完了できる形になりました。

まとめ

  • 責務をコンポーネント単位で切ることが、最大の改善効果をもたらす。 
    行数ではなく「誰が何を管理するか」を明確にするだけで、影響範囲が読みやすくなり保守性が劇的に向上します。
  • プロトタイプは早期リスク軽減の投資になる。 
    小さな検証を短期間で回せる体制を作ると、方針決定が早まり本実装の手戻りが減ります。
  • 生成AIは補助役として有効。 
    ただし安定して使うためにはプロトタイプ段階で「プロンプト/コンテキスト設計」を詰める必要がある。
  • 「実装が採用されない」ことは失敗ではない。 
    実装を通じて得た知見は次の改善に活かせる。学び自体が資産となる。

今回の経験で最も実感したのは、「責務をコンポーネント単位で切って、そこに必要なロジックだけを当てはめる」という単純な設計原則が、実運用での変更容易性とコードの読みやすさに直結するということです。ワイヤーフレーム起点で設計し、プロトタイプで早く検証し、得られた知見を確実に継承する。これを回すことで、API差し替えなど将来的な変更も最小限のコストで済ませられるようになりました。