---
title: "React"
description: "A React contact form for a single-page app, such as one built with Vite. It loads the bot check, sends JSON with fetch and shows the answer."
url: "https://formparcel.live/docs/guides/react"
---

# React

A React contact form for a single-page app, such as one built with Vite. It loads the bot check, sends JSON with fetch and shows the answer.

Untested: Not yet tested on a live site. The steps follow how the form address works today; if one differs, [tell us](https://formparcel.live/contact).

## The component

Paste this component and render it where the form goes. For Next.js, use [the Next.js guide](https://formparcel.live/docs/guides/nextjs).

Example form address. Use yours from the dashboard.

```jsx title="ContactForm.jsx"
import { useEffect, useState } from "react";

const ENDPOINT = "https://f.formparcel.live/f/f_k7QmXp2aVb9sTn4cLw8RhE";
// FormParcel's bot check: no cookies, no widget. Without it, submissions wait in your Spam folder.
const BOT_CHECK_SCRIPT = "https://f.formparcel.live/f/formparcel.js";

export function ContactForm() {
  const [status, setStatus] = useState("idle");

  useEffect(() => {
    if (document.querySelector('script[src="' + BOT_CHECK_SCRIPT + '"]')) return;
    const script = document.createElement("script");
    script.src = BOT_CHECK_SCRIPT;
    script.async = true;
    document.head.append(script);
  }, []);

  async function onSubmit(event) {
    event.preventDefault();
    setStatus("sending");
    const data = Object.fromEntries(new FormData(event.currentTarget));
    try {
      // One proof per submission. The bot check may still be loading on a slow phone, so wait for it (10 seconds at most). If it cannot load, the lead is still sent and waits in your Spam folder.
      const fp = window.FormParcel || await new Promise((ok) => { addEventListener("formparcel:ready", () => ok(window.FormParcel), { once: true }); setTimeout(ok, 10000); });
      const _fp_proof = await fp?.proof(ENDPOINT).catch(() => undefined);
      const response = await fetch(ENDPOINT, {
        method: "POST",
        headers: { "Content-Type": "application/json", Accept: "application/json" },
        body: JSON.stringify({ ...data, _fp_proof }),
      });
      // 204: the post was dropped as spam (the trap field was filled). Nothing was saved.
      const result = response.status === 204 ? null : await response.json();
      // "pending" is success too: the lead is stored. Never send it again.
      setStatus(result?.accepted ? "sent" : "error");
    } catch {
      setStatus("error");
    }
  }

  if (status === "sent") return <p>Thanks, we got your message.</p>;

  return (
    <form onSubmit={onSubmit}>
      {/* Spam trap: keep it hidden from people. */}
      <div aria-hidden="true" style={{ position: "absolute", left: "-10000px" }}>
        <input type="text" name="_gotcha" tabIndex={-1} autoComplete="off" />
      </div>
      <label htmlFor="name">Name</label>
      <input id="name" name="name" required />
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required />
      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" required />
      <button type="submit" disabled={status === "sending"}>
        {status === "sending" ? "Sending…" : "Send"}
      </button>
      {status === "error" ? <p role="alert">Your message was not sent. Please try again.</p> : null}
    </form>
  );
}
```

- A `200` with `accepted: true` is success, whether `deliveryStatus` is `"delivered"` or `"pending"`. Never send a pending lead again.
- A `204` means the hidden trap field was filled: nothing was saved.
- Every refusal and its code is on [Responses and errors](https://formparcel.live/docs/reference/responses).

> **Bot check.** A lead sent without FormParcel's bot check is saved but held in your Spam folder, not delivered. That is every post from a server, a webhook or `curl`, and every post from a page that does not load the script. The dashboard then shows **Bot check not installed** under Needs action. Fix: load the script on the page and send the proof from `FormParcel.proof()` with each `fetch`. [Learn more](https://formparcel.live/docs/reference/bot-check)

## Check it

Send a test lead from the dashboard, then submit the form once from the deployed site and open its receipt under **Leads**.
