JavaScript library
React form handling without a backend
A plain React SPA (Create React App, Vite, or a hand-rolled bundle) has no server of its own, which is usually the moment people reach for a third-party form backend. Formward is that backend: your component POSTs the form data to a single URL and gets back JSON, so you never stand up an Express server or a serverless function just to receive a contact form.
The component below is dependency-free — useState plus fetch. It captures the form with FormData, requests a JSON response, and tracks sending, success, and error so you can give the user real feedback instead of a full-page reload.
EU-hosted · A static React bundle plus an EU form endpoint means personal data is collected without ever touching infrastructure outside the EU/EEA.
The React example
Copy this into your project and replace <FORM_ID> with the id of a form you create in the Formward dashboard.
jsx
import { useState } from "react";
export function ContactForm() {
const [status, setStatus] = useState("idle"); // idle | sending | ok | error
async function onSubmit(e) {
e.preventDefault();
setStatus("sending");
const form = e.target;
try {
const res = await fetch("https://forms.formward.eu/f/<FORM_ID>", {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(form),
});
const data = await res.json(); // { ok: true, id, files }
if (!res.ok || !data.ok) throw new Error(data.error || res.status);
form.reset();
setStatus("ok");
} catch {
setStatus("error");
}
}
if (status === "ok") return <p>Thanks — your message is on its way.</p>;
return (
<form onSubmit={onSubmit}>
<input type="email" name="email" required />
<textarea name="message" required />
<input type="text" name="_gotcha" tabIndex={-1} autoComplete="off"
style={{ display: "none" }} aria-hidden="true" />
<button disabled={status === "sending"}>
{status === "sending" ? "Sending…" : "Send"}
</button>
{status === "error" && <p role="alert">Could not send. Try again.</p>}
</form>
);
}Success and error handling
- FormData reads every named field automatically, so adding inputs needs no extra JavaScript — just give each input a name attribute.
- Branch on both res.ok and data.ok: a network failure throws, and a rejected submission returns { ok: false, error } with a 4xx status.
- Because there is no server in the loop, there is nothing to deploy or patch — the only moving part is Formward's endpoint.
Spam protection
Every example above includes the _gotcha honeypot field. It is hidden from real users and must stay empty; Formward silently drops any submission where it is filled, which stops most bots with no CAPTCHA. For a stricter gate, add a Cloudflare Turnstile widget and send its token as the cf-turnstile-response field — Formward verifies it on receipt.
The JSON response
A standard POST gets a 302 redirect (or your thank-you page). Send the Accept: application/json header — as every fetch() example above does — and Formward returns JSON instead:
HTTP/1.1 200 OK
Content-Type: application/json
{ "ok": true, "id": "clxyz123...", "files": [] }On failure the body is { ok: false, error } with a 4xx status (validation, plan limit, and so on). See the AJAX docs for the full status-code table and CORS notes.
Other frameworks
Collect your first React submission
Create a form, paste the snippet, and keep every submission in the EU. GDPR-clean from the first POST.