React Guide

React Contact Form (with hooks & fetch)

Build a copy-pasteable React contact form that posts straight to Formigo - no backend of your own, no server route, no fuss. Just useState, fetch, and a form endpoint that actually answers.

1. Grab your form endpoint

Every Formigo form has its own endpoint that looks like https://formigo.io/f/your-form. Your React app posts a plain JSON body to that URL - Formigo stores the submission, emails you, and replies with a tidy 2xx. The endpoint is CORS-enabled, so a client-side fetch() from the browser works out of the box. No proxy route required.

Swap your-form for your real form slug from the Formigo dashboard. Everything else below is ready to paste.

2. The full React contact form component

ContactForm.jsx

Function component, hooks, success / error / rate-limit handling

Here is the whole thing - drop it into a .jsx file and render <ContactForm /> wherever you need it. It handles a successful send, a generic error, and Formigo's 429 rate-limit response, plus two quiet spam fields. We break down each piece right after.

import { useState, useEffect } from "react";

const FORM_ENDPOINT = "https://formigo.io/f/your-form";

export default function ContactForm() {
  const [formData, setFormData] = useState({
    name: "",
    email: "",
    message: "",
    _formigo_hp: "", // Honeypot: real humans leave this empty
  });
  const [status, setStatus] = useState("idle"); // idle | sending | success | error
  const [errorMsg, setErrorMsg] = useState("");
  const [loadedAt, setLoadedAt] = useState(0);

  // Stamp the moment the form mounted. Bots tend to submit instantly.
  useEffect(() => {
    setLoadedAt(Math.floor(Date.now() / 1000));
  }, []);

  const handleChange = (e) => {
    setFormData((prev) => ({ ...prev, [e.target.name]: e.target.value }));
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    setStatus("sending");
    setErrorMsg("");

    try {
      const response = await fetch(FORM_ENDPOINT, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          ...formData,
          _formigo_t: loadedAt, // Unix timestamp from when the form mounted
        }),
      });

      if (response.ok) {
        setStatus("success");
        setFormData({ name: "", email: "", message: "", _formigo_hp: "" });
        setLoadedAt(Math.floor(Date.now() / 1000));
      } else if (response.status === 429) {
        const data = await response.json();
        setStatus("error");
        setErrorMsg(`Whoa there - too many tries. Retry in ${data.retry_after}s.`);
      } else {
        setStatus("error");
        setErrorMsg("Something went sideways. Please try again.");
      }
    } catch (err) {
      setStatus("error");
      setErrorMsg("Network hiccup. Check your connection and retry.");
    }
  };

  return (
    <form onSubmit={handleSubmit} className="contact-form">
      <label htmlFor="name">Name</label>
      <input
        id="name"
        name="name"
        type="text"
        value={formData.name}
        onChange={handleChange}
        required
      />

      <label htmlFor="email">Email</label>
      <input
        id="email"
        name="email"
        type="email"
        value={formData.email}
        onChange={handleChange}
        required
      />

      <label htmlFor="message">Message</label>
      <textarea
        id="message"
        name="message"
        rows={5}
        value={formData.message}
        onChange={handleChange}
        required
      />

      {/* Honeypot: hidden from real users, catnip for bots */}
      <input
        type="text"
        name="_formigo_hp"
        value={formData._formigo_hp}
        onChange={handleChange}
        tabIndex={-1}
        autoComplete="off"
        aria-hidden="true"
        style={{ position: "absolute", left: "-9999px" }}
      />

      <button type="submit" disabled={status === "sending"}>
        {status === "sending" ? "Sending..." : "Send message"}
      </button>

      {status === "success" && (
        <p role="status" className="form-ok">
          Thanks! Your message landed safely.
        </p>
      )}

      {status === "error" && (
        <p role="alert" className="form-error">
          {errorMsg}
        </p>
      )}
    </form>
  );
}

3. How it works, step by step

Nothing magic here - just idiomatic modern React. Here is what each part is doing.

State with useState

One formData object holds your fields as a controlled form, and a small status string (idlesendingsuccess / error) drives the button label and the messages. Keeping status as a single value beats juggling three booleans.

The timestamp with useEffect

On mount we record the current Unix time in loadedAt and send it as _formigo_t. Bots usually post the instant a page loads; a human takes a few seconds to type, so the gap is a handy spam signal.

Posting with fetch

const response = await fetch("https://formigo.io/f/your-form", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ ...formData, _formigo_t: loadedAt }),
});

We send JSON, so we set Content-Type: application/json. (Formigo also accepts application/x-www-form-urlencoded if you would rather post a FormData body - either is fine.) Because the endpoint is CORS-enabled, this runs directly from the browser.

Handling the response

A successful submission is any 2xx, so response.ok is your green light - we show a thank-you and reset the fields. If you hit the rate limiter you get an HTTP 429 with a JSON body like { "retry_after": 30 }, so we read retry_after and tell the user exactly how long to wait. Anything else falls through to a friendly generic error, and a network failure is caught by the surrounding try / catch.

The honeypot

_formigo_hp is a real input pushed off-screen with position: absolute; left: -9999px and marked aria-hidden with tabIndex={-1}. People never see or focus it, so it stays empty. Bots that blindly fill every field give themselves away - a non-empty honeypot is a strong spam signal. Together with the timestamp, that is most of your spam gone without a single CAPTCHA.

Using Next.js? This same component works in the App Router as a Client Component - just add "use client" at the top of the file.

Wire up your React contact form in minutes

Create a free Formigo form, paste your formigo.io/f/ endpoint into the component above, and start collecting submissions today. No backend to build, no spam to wade through.