← All frameworks

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>Thanksyour 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.

React form handling without a backend | Formward