Get the next one in your inbox.
One email when we publish something worth reading. No spam — appropriately enough, we'd know.
A React pattern for multi-step forms that accumulates answers in local state across steps and submits the combined data to FormBridge only once, at the end.

Get an instant summary, key takeaways, action items, and answers to your questions about this article.
Keep every step's answers in one component state object as the user moves forward and back, and only call fetch() to your FormBridge endpoint once, on the final step, with all the accumulated fields combined into a single request body.
A multi-step form doesn't need a form library or multiple endpoints — just component state that accumulates across steps, submitted once at the end.
'use client';
import { useState } from 'react';
const ENDPOINT = 'https://app.formbridge.ai/api/forms/fb_8h2k9p';
const initialState = { fullName: '', email: '', companySize: '', useCase: '' };
export default function MultiStepForm() {
const [step, setStep] = useState(1);
const [data, setData] = useState(initialState);
const [submitted, setSubmitted] = useState(false);
function update(field, value) {
setData((prev) => ({ ...prev, [field]: value }));
}
async function handleFinalSubmit() {
const res = await fetch(ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({
'full-name': data.fullName,
email: data.email,
'company-size': data.companySize,
'use-case': data.useCase,
}),
});
if (res.ok) setSubmitted(true);
}
if (submitted) return <p>Thanks, {data.fullName} — we've got everything we need.</p>;
return (
<div>
{step === 1 && (
<fieldset>
<input placeholder="Full name" value={data.fullName} onChange={(e) => update('fullName', e.target.value)} />
<input placeholder="Email" value={data.email} onChange={(e) => update('email', e.target.value)} />
<button onClick={() => setStep(2)}>Next</button>
</fieldset>
)}
{step === 2 && (
<fieldset>
<input placeholder="Company size" value={data.companySize} onChange={(e) => update('companySize', e.target.value)} />
<button onClick={() => setStep(1)}>Back</button>
<button onClick={() => setStep(3)}>Next</button>
</fieldset>
)}
{step === 3 && (
<fieldset>
<textarea placeholder="What are you building?" value={data.useCase} onChange={(e) => update('useCase', e.target.value)} />
<button onClick={() => setStep(2)}>Back</button>
<button onClick={handleFinalSubmit}>Submit</button>
</fieldset>
)}
</div>
);
}
FormBridge processes each POST as one complete submission — spam check, notification, autoresponder — so posting on every step would create three partial, spammy-looking submissions instead of one clean one. Keep every step's answer in local state (or sessionStorage if you want it to survive a refresh) and fire the single fetch call only from the last step.
The component's internal state can use whatever casing is convenient (companySize), but map it to hyphenated field names (company-size) right before you send the request — that's what determines the label and merge tag ({{Company-Size}}) shown in the inbox and available in notification and autoresponder templates.
For a form long enough that people abandon and return, write data to sessionStorage on every update call and read it back in a useEffect on mount — FormBridge only ever sees the final, combined payload, so nothing about the backend changes.
No. Submitting per step creates multiple partial, incomplete-looking entries in your inbox. Accumulate answers in state and send one combined submission from the last step.
Mirror the state object into sessionStorage on every update and read it back on mount — this only affects your component's local state and has no effect on FormBridge, which still only ever receives one final submission.
One email when we publish something worth reading. No spam — appropriately enough, we'd know.