How do I build a multi-step form with branching in React or Vue?
With Formhaus, the branching lives in the JSON definition: each step lists routes, and the first route whose show conditions match picks the next step. FormRenderer from @formhaus/react or @formhaus/vue renders the steps, validates each one on Continue, handles Back and Skip, and submits only the answers on the path the user took.
Definition
The service answer sends the user to a design or a development step. Both branches converge at an optional budget step, then contact details.
json
{
"$schema": "https://formhaus.dev/schema/form-definition.json",
"id": "project-inquiry",
"title": "Start a project",
"submit": { "label": "Send inquiry" },
"steps": [
{
"id": "service",
"title": "What do you need?",
"fields": [
{
"key": "service",
"type": "radio",
"label": "Service",
"validation": { "required": true },
"options": [
{ "value": "design", "label": "Design" },
{ "value": "development", "label": "Development" }
]
}
],
"routes": [
{ "to": "design", "show": [{ "field": "service", "eq": "design" }] },
{ "to": "development", "show": [{ "field": "service", "eq": "development" }] }
]
},
{
"id": "design",
"title": "Design scope",
"fields": [
{ "key": "pages", "type": "number", "label": "Number of screens", "validation": { "required": true, "min": 1 } },
{ "key": "hasBrand", "type": "switch", "label": "We have brand guidelines" }
],
"routes": [{ "to": "budget" }]
},
{
"id": "development",
"title": "Development scope",
"fields": [
{
"key": "platform",
"type": "select",
"label": "Platform",
"validation": { "required": true },
"options": [
{ "value": "web", "label": "Web" },
{ "value": "ios", "label": "iOS" },
{ "value": "android", "label": "Android" }
]
},
{ "key": "repoUrl", "type": "text", "label": "Existing repository URL" }
],
"routes": [{ "to": "budget" }]
},
{
"id": "budget",
"title": "Budget",
"skip": { "label": "Not sure yet" },
"fields": [
{
"key": "budget",
"type": "radio",
"label": "Budget range",
"validation": { "required": true },
"options": [
{ "value": "small", "label": "Under $10k" },
{ "value": "medium", "label": "$10k to $50k" },
{ "value": "large", "label": "Over $50k" }
]
}
]
},
{
"id": "contact",
"title": "Contact details",
"back": { "label": "Edit answers" },
"fields": [
{ "key": "name", "type": "text", "label": "Name", "validation": { "required": true } },
{ "key": "email", "type": "email", "label": "Work email", "validation": { "required": true } }
]
}
]
}React
tsx
import { FormRenderer } from '@formhaus/react';
import type { FormDefinition } from '@formhaus/core';
import inquiry from './project-inquiry.json';
const definition = inquiry as FormDefinition;
export function ProjectInquiry() {
return (
<FormRenderer
definition={definition}
onStepChange={(stepId, direction) => console.log(stepId, direction)}
onSubmit={async (values) => {
await fetch('/api/inquiries', { method: 'POST', body: JSON.stringify(values) });
}}
/>
);
}Vue
vue
<script setup lang="ts">
import { FormRenderer } from '@formhaus/vue';
import type { FormDefinition } from '@formhaus/core';
import inquiry from './project-inquiry.json';
const definition = inquiry as FormDefinition;
async function save(values: Record<string, unknown>) {
await fetch('/api/inquiries', { method: 'POST', body: JSON.stringify(values) });
}
</script>
<template>
<FormRenderer :definition="definition" :submit-handler="save" />
</template>How it works
routeson theservicestep are checked in order.{ "field": "service", "eq": "design" }sends the user todesign; thedevelopmentroute matches the other answer.designanddevelopmenteach end with{ "to": "budget" }. Without that route,designwould fall through to the next declared step,development, and both branches would run.- Route targets must be later declared steps. Backward targets and unknown step ids throw when the engine is created and are reported by
validateDefinition(). skiponbudgetrenders a Skip button labelled "Not sure yet". Skip resets the step to its defaults and leaves its fields out of validation and the submitted values.backoncontactrelabels the Back button. Back follows the path the user took, so fromcontactit returns tobudget, then to the branch step.- Progress shows "Step N of 4" for either branch. Before the service is answered the path is provisional.
- Answers from a branch the user left stay in
engine.valuesas a draft but are not validated or submitted. Choosing development, filling the platform and switching to design submitspages, notplatform.
Related
- Multi-Step Forms: routes, skip, lifecycle hooks and
autoAdvance - Conditional Fields: the
showandshowAnyoperators routes use - Async Step Validation: server checks before the next step
- Definition Reference: the
StepRoutetype - FormEngine:
nextStepAsync,prevStepAsyncandskipStepAsync