React/Next.jsで動的フォームを構築する2つの手法——RHF+ZodとSurveyJSの比較

React/Next.jsで動的フォームを構築する2つの手法——RHF+ZodとSurveyJSの比較

React/Next.jsで動的フォームを構築する2つの手法——RHF+ZodとSurveyJSの比較

Reactアプリケーションにおいて、フォームの実装は避けて通れない課題だ。ログイン画面のような単純なものから、条件分岐が複雑に絡み合う多ステップの入力フォームまで、その難易度は多岐にわたる。

多くの開発者が「フォームはコンポーネントとして構築すべきだ」という共通のメンタルモデルを持っている。React Hook Form(RHF)とZodを組み合わせる手法は、現在のReactエコシステムにおける標準的な選択肢となっている。

しかし、フォームが「UI」の枠を超え、複雑な「ルールエンジン」へと変貌したとき、このモデルは限界を迎える。本記事では、コンポーネント駆動とスキーマ駆動という2つの異なるアプローチを比較し、プロジェクトに最適な手法を選択するための基準を提示する。

コンポーネント駆動アプローチ:React Hook FormとZodの組み合わせ

コンポーネント駆動アプローチ:React Hook FormとZodの組み合わせ

React Hook Form(RHF)は、非制御コンポーネントを活用して再レンダリングを最小限に抑えるライブラリだ。これにスキーマバリデーションライブラリであるZodを組み合わせることで、型安全で直感的なフォーム実装が可能になる。

Zodによるバリデーションスキーマの定義

コンポーネント駆動開発では、まずZodでデータの形状(Shape)を定義する。単純な必須チェックだけでなく、特定の条件に基づいた相関バリデーションも記述できる。

import { z } from "zod";

export const formSchema = z.object({
  firstName: z.string().min(1, "必須項目です"),
  email: z.string().email("無効なメール形式です"),
  price: z.number().min(0),
  quantity: z.number().min(1),
  taxRate: z.number(),
  hasAccount: z.enum(["Yes", "No"]),
  username: z.string().optional(),
  password: z.string().optional(),
  satisfaction: z.number().min(1).max(5),
  positiveFeedback: z.string().optional(),
  improvementFeedback: z.string().optional(),
}).superRefine((data, ctx) => {
  if (data.hasAccount === "Yes") {
    if (!data.username) {
      ctx.addIssue({ code: "custom", path: ["username"], message: "必須項目です" });
    }
    if (!data.password || data.password.length < 6) {
      ctx.addIssue({ code: "custom", path: ["password"], message: "6文字以上必要です" });
    }
  }
});

export type FormData = z.infer<typeof formSchema>;

このコードでは、`superRefine` を使用して「アカウントあり(Yes)」の場合にのみユーザー名とパスワードを必須にするロジックを実装している。Zodのスキーマはデータの構造を定義するものであり、どのフィールドがどのタイミングで表示されるかといった「表示ロジック」までは関与しない。

RHFによるフォームの実装と課題

RHFを用いた実装では、フォームの表示状態やステップの管理をReactの `useState` や `useWatch` で行う。

const { register, control, handleSubmit } = useForm<FormData>({
  resolver: zodResolver(formSchema),
});

const hasAccount = useWatch({ control, name: "hasAccount" });
const price = useWatch({ control, name: "price" });
const quantity = useWatch({ control, name: "quantity" });

const subtotal = useMemo(() => (price ?? 0) * (quantity ?? 1), [price, quantity]);

この手法の利点は、Reactの標準的な作法に従っているため、学習コストが低く自由度が高い点にある。一方で、条件分岐が増えるにつれてJSX内にインラインの条件式が散在し、ビジネスロジックがUIコードと密結合になるという課題がある。

スキーマ駆動アプローチ:SurveyJSによるデータ中心の実装

スキーマ駆動アプローチ:SurveyJSによるデータ中心の実装

スキーマ駆動アプローチでは、フォームをコンポーネントの集合体ではなく、一つの「データ(JSONスキーマ)」として扱う。SurveyJSはこの手法を代表するライブラリであり、フォームの構造、バリデーション、表示ロジック、計算式をすべてJSON内に集約できる。

JSONスキーマによるロジックの集約

SurveyJSでは、JavaScriptのコードではなく、宣言的なJSONオブジェクトでフォームを定義する。

const surveySchema = {
  pages: [
    {
      name: "order",
      elements: [
        { type: "text", name: "price", inputType: "number" },
        { type: "text", name: "quantity", inputType: "number" },
        {
          type: "expression",
          name: "subtotal",
          expression: "{price} * {quantity}"
        }
      ]
    },
    {
      name: "account",
      elements: [
        { type: "radiogroup", name: "hasAccount", choices: ["Yes", "No"] },
        {
          type: "text",
          name: "username",
          visibleIf: "{hasAccount} = 'Yes'",
          isRequired: true
        }
      ]
    }
  ]
};

ここで注目すべきは `visibleIf` や `expression` というプロパティだ。これらはSurveyJSのランタイムエンジンによって評価され、Reactのステート管理を介さずに動的な表示切り替えや計算結果の表示を可能にする。

Reactコンポーネントの役割の変化

SurveyJSを採用すると、Reactコンポーネントの役割は「スキーマを読み込んで描画するだけ」という極めてシンプルなものになる。

import { Model } from "survey-core";
import { Survey } from "survey-react-ui";

export function SurveyForm() {
  const [model] = useState(() => new Model(surveySchema));

  model.onComplete.add((sender) => {
    console.log(sender.data); // 送信データ
  });

  return <Survey model={model} />;
}

ビジネスロジックがReactから切り離されるため、エンジニア以外の担当者がJSONを書き換えるだけでフォームの挙動を調整できる。これは、要件変更が頻繁に発生するエンタープライズ向けのシステムにおいて、運用上の大きなメリットとなる。

技術的なトレードオフと選択基準

技術的なトレードオフと選択基準

どちらのアプローチが優れているかは、プロジェクトの性質に依存する。ここでは、開発効率と保守性の観点から両者を比較する。

コンポーネント駆動(RHF+Zod)が適しているケース

RHF+Zodのスタックは、フォームが単純なCRUD(作成・読み取り・更新・削除)操作に限定されている場合に最適だ。

  • UIの微調整や独自のアニメーションを多用する場合
  • フォームのステップ数が少なく、条件分岐が単純な場合
  • エンジニアがすべての挙動をコードで管理し、外部からロジックを注入する必要がない場合

このアプローチでは、TypeScriptの恩恵を最大限に受けられる。型定義が明確であるため、大規模なリファクタリングにも強いという特性がある。

スキーマ駆動(SurveyJS)が適しているケース

一方で、SurveyJSのようなスキーマ駆動が威力を発揮するのは、フォーム自体が「ビジネスルール」を体現している場合だ。

  • 法規制や業務フローの変化により、入力項目や条件が頻繁に変わる場合
  • 10ステップを超えるような長大な多ステップフォームを構築する場合
  • フォームの定義をデータベースに保存し、複数のプラットフォーム(Web、モバイルなど)で共有する場合

スキーマ駆動は、ロジックの「可視化」に優れている。JSONを見ればどの項目がどの条件で表示されるかが一目瞭然であり、コードを追う必要がない。

実務における運用上のメリットと分析

実務における運用上のメリットと分析

Web制作会社の視点では、保守コストの削減が最も重要な関心事となる。コンポーネント駆動で構築された複雑なフォームは、半年後の仕様変更時に「どの `useWatch` がどこに影響しているか」を解読する作業から始めなければならない。

スキーマ駆動を採用した場合、ロジックが中央集権的に管理されているため、修正箇所が限定される。また、SurveyJSにはGUIのビジュアルエディタも存在するため、非エンジニアであるディレクターやクライアントが直接フォームを編集し、エンジニアは生成されたJSONを組み込むだけという分業体制も構築可能だ。

ただし、SurveyJSのような重量級のライブラリは、バンドルサイズが増大する傾向にある。パフォーマンスが最優先されるBtoCのランディングページなどでは、RHFを用いた軽量な実装が選好されるだろう。

この記事のポイント

  • RHF+Zodは、UIの自由度が高く、小〜中規模のCRUDフォームに最適である。
  • SurveyJSは、ビジネスロジックをJSONに集約し、複雑な条件分岐を持つフォームの保守性を高める。
  • 「フォームを消したときに失われるものがUIならRHF、ロジックならSurveyJS」という判断基準が有効だ。
  • 運用フェーズでの要件変更の頻度を予測し、適切な抽象化レベルを選択することが重要である。

出典

  • Smashing Magazine “Building Dynamic Forms In React And Next.js”(2026年3月10日)
海田 洋祐

・ 複数業界における17年間のデジタルビジネス開発経験 ・ ウェブサイト開発のためのHTML、PHP、CSS、Java等の実用的知識 ・ 15ヶ国語対応の多言語SaaSの開発経験 ・ 17年間にも及ぶ、Eコマース長期運営経験 ・ 幅広い業界でのSEO最適化の豊富な経験

メッセージを残す