Skip to content

Formhaus vs react-hook-form ​

react-hook-form is a hook for forms you write in React code; Formhaus is a JSON definition that an engine runs and React, Vue and Figma render. For one hand-written React form, react-hook-form is the more flexible choice with a smaller API surface. For multi-step flows with branching, forms stored as data, or forms that also live in Figma, Formhaus does that work without extra code.

Choose react-hook-form when ​

  • The form is written and maintained by developers in React, and its markup is custom.
  • You already validate with Zod, Yup or another schema library. react-hook-form plugs them in through a resolver.
  • You need field arrays, fine-grained re-render control or a large ecosystem of examples. The useFieldArray hook covers dynamic lists.
  • The app is React only. react-hook-form supports React 16.8 through 19 (npm).

Choose Formhaus when ​

  • The form is data: it comes from a CMS, an API, a product team or an AI agent, and changes without a deploy.
  • The flow has steps that depend on answers. Formhaus has steps, routes and conditional steps in the definition.
  • The same form must render in React and Vue, or be drawn in Figma from the same file.
  • AI agents generate or edit forms. The JSON Schema and the MCP server check their output against the real engine.

Feature comparison ​

Formhausreact-hook-form
Definition formatJSON, checked by a JSON SchemaReact code with register and useForm (docs)
Multi-stepBuilt in: steps, per-step validation, progress (guide)Pattern: one form per page, answers kept in a state library (Wizard Form / Funnel)
Branching routesroutes on a step (guide)Written in your router or state code (Wizard Form / Funnel)
Conditional fieldsshow / showAny; hidden values cleared (guide)Render based on watch; unmounted values dropped with shouldUnregister
ValidationDeclarative rules, named validators, async step validation (guide)Rules in register, or a schema resolver (docs)
Custom componentscomponents map per field type (guide)Any component through Controller
FigmaPlugin draws the definition (guide)Not documented
AI toolingJSON Schema, MCP server, Claude Code pluginForms are code; a web Form Builder generates React code
FrameworksReact 18+, Vue 3.3+, headless engineReact (npm)
Bundle size (gzipped)6.2 KB core + 4.8 KB React14.4 KB for 7.89.0 (bundlephobia)
LicenseMITMIT (npm)

Sizes measured on 2026-10-09. See how Formhaus sizes are measured.

Migration ​

The same sign-up form: email, plan, and a company field only for the Team plan.

react-hook-form ​

tsx
import { useForm } from 'react-hook-form';

type Values = { email: string; plan: string; company?: string };

export function Signup({ onDone }: { onDone: (values: Values) => void }) {
  const { register, handleSubmit, watch, formState: { errors } } = useForm<Values>({
    shouldUnregister: true,
  });
  const plan = watch('plan');

  return (
    <form onSubmit={handleSubmit(onDone)}>
      <label>
        Email
        <input
          type="email"
          {...register('email', {
            required: 'Enter your email',
            pattern: { value: /^\S+@\S+$/, message: 'Enter a valid email' },
          })}
        />
      </label>
      {errors.email && <p>{errors.email.message}</p>}
      <label>
        Plan
        <select {...register('plan', { required: 'This field is required' })}>
          <option value="">Choose</option>
          <option value="solo">Solo</option>
          <option value="team">Team</option>
        </select>
      </label>
      {errors.plan && <p>{errors.plan.message}</p>}
      {plan === 'team' && (
        <label>
          Company
          <input {...register('company', { required: 'Enter your company' })} />
        </label>
      )}
      {errors.company && <p>{errors.company.message}</p>}
      <button type="submit">Create account</button>
    </form>
  );
}

Formhaus ​

json
{
  "id": "signup",
  "title": "Sign up",
  "submit": { "label": "Create account" },
  "fields": [
    {
      "key": "email",
      "type": "email",
      "label": "Email",
      "validation": {
        "required": "Enter your email",
        "pattern": "^\\S+@\\S+$",
        "patternMessage": "Enter a valid email"
      }
    },
    {
      "key": "plan",
      "type": "select",
      "label": "Plan",
      "options": [
        { "value": "solo", "label": "Solo" },
        { "value": "team", "label": "Team" }
      ],
      "validation": { "required": true }
    },
    {
      "key": "company",
      "type": "text",
      "label": "Company",
      "show": [{ "field": "plan", "eq": "team" }],
      "validation": { "required": "Enter your company" }
    }
  ]
}
tsx
import { FormRenderer } from '@formhaus/react';
import type { FormDefinition } from '@formhaus/core';
import signup from './signup.json';

const definition = signup as FormDefinition;

export function Signup({ onDone }: { onDone: (values: Record<string, unknown>) => void }) {
  return <FormRenderer definition={definition} onSubmit={onDone} />;
}

Rules move from register options to validation, the watch check becomes a show condition, and labels, options and error rendering come from the definition. To keep your own inputs, map them with the components prop.