Forms As Components vs. Forms As Data

Most React forms follow a well-worn pattern: React Hook Form for local state, Zod for validation, React Query for submission. That stack handles login screens, settings pages, and CRUD modals cleanly. Each library has a defined job, the pieces compose, and you move on.

The model breaks down when a form stops being a static collection of inputs and starts behaving like a decision engine. Visibility rules depend on earlier answers. Derived values cascade through multiple fields. Pages appear or disappear based on computed totals. What began as a component tree quietly becomes a state machine — but the abstractions you're using still assume simple UI.

There are two fundamentally different ways to build such forms. The first treats the form as a composition of React components with logic spread across hooks, validation schemas, and JSX. The second treats the form as a JSON schema — data that a runtime engine evaluates — with React only handling rendering. To compare them directly, we built the same multi-step form both ways.

The form has four steps:

  • Details: first name (required), email (required, valid format).
  • Order: unit price, quantity, tax rate; derived subtotal, tax, and total.
  • Account & Feedback: conditional username/password if the user has an account; feedback questions appear based on a 1–5 satisfaction rating.
  • Review: visible only when total >= 100, followed by submission.

The requirements aren't extreme, but they expose where each architecture starts to strain.

Component-Driven: React Hook Form + Zod

In the component-driven version, the Zod schema handles the static shape — required strings, numbers with minimums, an enum. The conditional parts need a different mechanism.

Fields like username and password must be typed as optional() in the schema, even though they're conditionally required. Zod's type-level schema describes an object's shape, not the rules governing when fields matter. Cross-field requirements live inside superRefine, which runs after shape validation and sees the full object. That's not a flaw — it's exactly where cross-field logic belongs when it can't live in the schema structure itself.

The schema can't express anything about pages, field visibility, or navigation. All of that moves into the component layer:

  • Derived values (subtotal, tax, total) are computed via useWatch and useMemo inside the component.
  • Visibility rules for conditional fields live as inline JSX conditionals.
  • Review-page skipping is embedded in a showSubmit variable and a render branch.
  • Step navigation is a manually incremented useState counter.

None of this is wrong. It's idiomatic React, and RHF's re-render isolation keeps it performant. But describe how this form actually behaves to someone who didn't write it — say, under what conditions the review page appears — and they'd have to trace through three separate locations to reconstruct a rule that could be stated in one line. The behavior isn't inspectable; it has to be executed mentally.

Any change, even adjusting when the review step shows, means editing the component, opening a pull request, and deploying. Form behavior requires engineering involvement for even small tweaks.

Schema-Driven: SurveyJS

SurveyJS approaches the problem from the opposite direction. Two packages are involved: the MIT-licensed survey-core runtime engine, which takes a JSON schema and builds an internal model — evaluating visibility expressions, computing derived values, managing pages, validating answers — and survey-react-ui, the thin rendering layer that connects that model to React. Similar UI libraries exist for Angular, Vue, and other frameworks.

That's the key architectural difference: all form logic lives in JSON data, not in React code.

Swap the RHF component for a JSON schema, and you get a different distribution of responsibilities:

  • The superRefine block conditionally requiring username and password disappears. A visibleIf: "{hasAccount} = 'Yes'" expression combined with isRequired: true handles both concerns where you'd expect to find them — on the field itself.
  • The useWatch / useMemo chain computing derived totals becomes three fields of type: 'expression' that reference each other by name. These expression fields are read-only and exist to display calculated values.
  • The review-page condition and navigation buttons become a single visibleIf property on the page object instead of scattered component logic.

The logic is identical, but visible in isolation. The schema gives each rule a home where it can be inspected directly.

What Remains in React

On the rendering side, little is left. The React component wires the model to the view and handles submission. The onComplete event fires when the user finishes the last visible page — the engine evaluates visibility before determining what "last page" means, so a skipped review page doesn't break completion. The payload contains all answers plus calculated values like subtotal, tax, and total as first-class fields — the same payload the RHF version assembled manually. A mutationRef pattern keeps the submission handler stable across renders.

What's gone: useWatch, conditional JSX, the step counter, the useMemo chain, superRefine. React handles what it does well — rendering and application integration — while form logic lives as data.

Because the schema is just JSON, it can live in a database, version independently of application code, or be edited through internal tools without a new deploy. A product manager tweaking the threshold that triggers the review page doesn't need a developer. That operational difference matters when form behavior evolves frequently and isn't always driven by engineers.

Choosing Between Component-Driven and Schema-Driven Forms

A practical way to decide: imagine the form is gone entirely. If what you lose is screens — fields, layouts, whole pages — component-driven forms with React Hook Form are the right fit. If what you lose is encoded business logic — thresholds, branching rules, conditional requirements that reflect real decisions — a schema engine is what you actually need.

The two models exist to solve different problems, and the real risk is mismatching the abstraction to the weight of the logic. Reaching for a rule engine because a form grew to three steps is as wrong as treating a policy system like a component because that pattern feels familiar. The example form in this article sits deliberately near that boundary: complex enough to show the difference, but not so extreme that the comparison feels one-sided.

Look at the shape of your upcoming work. If changes are mostly labels, fields, and arrangement, React Hook Form will carry you. If the changes are conditions, outcomes, and rules that operations or legal might need to adjust without filing a ticket, the schema-based model with SurveyJS is the honest fit.

React Hook Form + Zod Works When:

  • Forms are CRUD-oriented with shallow, UI-driven logic.
  • Engineers own every aspect of behavior.
  • The backend stays the source of truth.

SurveyJS Works When:

  • Forms encode genuine business decisions and branching rules.
  • Those rules evolve independently of UI structure.
  • Logic must be visible, auditable, or versioned.
  • Non-engineers need influence over behavior.
  • One form must run across multiple frontends.

Neither approach competes with the other; they serve different classes of problems. Most forms that have become unwieldy in a codebase are sitting near that same boundary, and the question is usually whether anyone has clearly named what they are.

Smashing Editorial