React Form Integration with Form Backend Tool

A React form backend does not need a package, and it does not need a server or API route of your own. There is no Form Backend SDK for React; the integration is a hosted HTTP endpoint, and React talks to it the two ways any web page can: with a plain form element, or with fetch. The endpoint stores each submission and emails it to you, as the concept article explains. This guide covers the React side of that: which mode to choose, a complete working component, and the failures React developers actually hit. The underlying field and response contract is the HTML integration reference; the first-form walkthrough is the contact form tutorial.

Two ways to submit from React

  • Plain form element. The browser submits the form itself and navigates: to your _redirect page, or to a built-in thank-you page. No state, no submit handler, nothing to break. Choose it when leaving the page after sending is fine, which is most contact pages.
  • fetch from a submit handler. Your code sends JSON and the page stays put, so you can show an inline confirmation, keep the user's context, and react to errors in the UI. Choose it for forms inside an app, a multi-step flow, or anywhere a full page load would feel wrong.

The endpoint treats them differently on purpose: a request that asks for JSON (the Accept header below) gets a JSON answer; a browser form submission gets the navigation behaviour. Mixing them up is the most common integration bug, and the troubleshooting section covers it.

Option 1: the plain form element

In JSX, the HTML pattern from the docs example works as written. React renders a real form element, and the browser does the rest:

ContactForm.jsx
<form action="https://form-backend-tool.vercel.app/api/forms/YOUR_FORM_KEY" method="POST">
  <label>
    Your name <input type="text" name="name" required />
  </label>
  <label>
    Your email <input type="email" name="email" required />
  </label>
  <label>
    Message <textarea name="message" required />
  </label>
  <input type="hidden" name="_redirect" value="https://your-site.example/thanks" />
  <input type="text" name="_honey" style={{ display: "none" }} tabIndex={-1} autoComplete="off" />
  <button type="submit">Send</button>
</form>

Notes for React specifically: the inputs are uncontrolled and need no state at all; name attributes are what the browser sends, so they are not optional; two JSX details differ from plain HTML, style takes an object and tabIndex is camelCase. The form key in the endpoint URL is public by design, since it ships in the rendered page, exactly like the key in the homepage live demo.

Option 2: fetch with submission state

This is the complete component, plain React with no libraries. Its exact request, including the empty honeypot field, was run against a live endpoint while this guide was written, and the answers quoted on this page are the answers it received. In Next.js it belongs in a client component; anywhere else it is an ordinary component.

ContactForm.jsx
import { useState } from "react";

const ENDPOINT = "https://form-backend-tool.vercel.app/api/forms/YOUR_FORM_KEY";

export default function ContactForm() {
  const [status, setStatus] = useState("idle"); // idle | sending | sent | error
  const [error, setError] = useState("");

  async function handleSubmit(event) {
    event.preventDefault();
    setStatus("sending");
    setError("");

    const form = event.currentTarget;
    const data = Object.fromEntries(new FormData(form).entries());

    try {
      const response = await fetch(ENDPOINT, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Accept: "application/json",
        },
        body: JSON.stringify(data),
      });
      const result = await response.json();
      if (response.ok && result.ok) {
        setStatus("sent");
        form.reset();
      } else {
        setStatus("error");
        setError(result.error || "Something went wrong. Please try again.");
      }
    } catch {
      setStatus("error");
      setError("Network error. Please check your connection and try again.");
    }
  }

  if (status === "sent") {
    return <p>Thanks, your message has been sent.</p>;
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Your name <input type="text" name="name" required />
      </label>
      <label>
        Your email <input type="email" name="email" required />
      </label>
      <label>
        Message <textarea name="message" required />
      </label>
      {/* Honeypot: hidden and always empty. Bots fill it in and are
          flagged as spam; the endpoint answers them like a success. */}
      <input type="text" name="_honey" style={{ display: "none" }} tabIndex={-1} autoComplete="off" />
      <button type="submit" disabled={status === "sending"}>
        {status === "sending" ? "Sending..." : "Send"}
      </button>
      {status === "error" && <p role="alert">{error}</p>}
    </form>
  );
}

How it works, piece by piece:

  • event.preventDefault() stops the browser's own submission. Without it the page navigates away (the Option 1 behaviour) and your fetch races the reload.
  • FormData reads the fields by their name attributes. The inputs stay uncontrolled; React only manages the submission status. If you prefer controlled inputs, keep the values in state and build the same object from state instead. The request on the wire is identical either way.
  • Accept: application/json is the switch that makes the endpoint answer with JSON instead of the browser navigation behaviour.
  • Success and failure are both read from the answer: the HTTP status (response.ok) and the body's ok flag, with the endpoint's own error message shown to the user when there is one. Nothing is assumed.
  • The honeypot rides along empty. FormData includes it as an empty string, which the endpoint treats as a human submission. A filled one is flagged as spam on the dashboard side while still answering like a success, so bots learn nothing.

A lead or quote form is the same component with more named fields; nothing else changes. For several forms, create one endpoint per form in the dashboard and point each component at its own, as the static websites guide describes for sites.

Reading the response

  • Success: HTTP 200, {"ok": true, "message": "Submission received."}
  • Failure: a 4xx or 5xx status with {"ok": false, "error": "..."}, for example {"ok": false, "error": "Form not found."} for a wrong key (404), or "Submission is empty." (400). The component above displays that message, which makes most integration mistakes self-explaining during development.
  • CORS: the endpoint answers with Access-Control-Allow-Origin: *, observed on the live responses while testing this guide, so a browser fetch from your own domain is allowed to read the answer. There is no domain allowlist to configure.

The full status table lives in the docs error reference.

Troubleshooting, React edition

  • The page reloads even though I used fetch. The submit handler is missing, or preventDefault() is not being called, so the browser performs its default form submission on top of yours.
  • fetch gets an HTML thank-you page instead of JSON. The Accept: application/json header is missing, so the endpoint used the browser-form behaviour. This is the single most common React integration mistake.
  • Submissions arrive with missing fields. An input without a name is invisible to FormData and to a plain form alike. Controlled inputs are the usual hiding place: value and onChange are set, but name was never added. With a hand-built JSON object, check the keys instead; they play the role of names.
  • 404 "Form not found." The key in the endpoint URL is wrong or belongs to a deleted form. Copy the endpoint from the form's dashboard page.
  • 405 Method not allowed. The request was not a POST. On a plain form element this means method="POST" is missing, and a form without it defaults to GET.
  • _redirect is ignored in fetch mode. Correct, and by design: the redirect belongs to the browser navigation flow. In fetch mode your code decides what happens next, like the "sent" state in the example.
  • 429 while testing. The endpoint allows 10 submissions per 60 seconds per form per visitor. A resubmit-on-render bug or an enthusiastic test loop reaches it quickly; wait a minute and it clears.

Limits

  • File uploads are not stored: a React file input can be sent, but the file itself is ignored and only text values are kept.
  • No outgoing webhooks and no autoresponder email to the visitor. The confirmation is your UI state or your redirect page.
  • At most 25 KB per submission, 50 fields, names up to 100 characters, values up to 5,000 characters.
  • Rate limit of 10 submissions per 60 seconds per form per visitor; monthly quotas per form of 250 on Free, 2,500 on Pro ($12/month) and 10,000 on Agency ($35/month).

The authoritative list is the docs limits section with the not supported list. Anything this guide does not answer is one message away on the support page.