# React and Next.js contact form

Last reviewed: 2026-10-16. Uses React hooks and the Forms API `/v1` contract,
not a Basestack Forms SDK. In Next.js App Router, interactive event handlers
belong in a `"use client"` component; in a React SPA the same component works.
Pass the real dashboard endpoint as `endpoint`. It is a public submission URL,
not a private account credential.

```tsx
"use client";

import { useState, type FormEvent } from "react";

export function ContactForm({ endpoint }: { endpoint: string }) {
  const [pending, setPending] = useState(false);
  const [status, setStatus] = useState("");
  const [error, setError] = useState("");

  async function submit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    if (pending) return;
    const form = event.currentTarget;
    const fields = new FormData(form);
    setPending(true);
    setStatus("");
    setError("");
    try {
      const url = new URL(endpoint);
      url.searchParams.set("mode", "rest");
      const response = await fetch(url, {
        method: "POST",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          name: fields.get("name"),
          email: fields.get("email"),
          message: fields.get("message"),
        }),
      });
      const body: unknown = await response.json();
      if (
        !response.ok ||
        typeof body !== "object" ||
        body === null ||
        !("success" in body) ||
        body.success !== true
      ) {
        throw new Error("Submission was not accepted.");
      }
      setStatus("Your message was accepted.");
      form.reset();
    } catch {
      setError(
        "We could not confirm submission. Your input is still here. Check before retrying.",
      );
    } finally {
      setPending(false);
    }
  }

  return (
    <form onSubmit={submit} aria-busy={pending}>
      <fieldset disabled={pending}>
        <legend>Contact us</legend>
        <label htmlFor="contact-name">Name</label>
        <input id="contact-name" name="name" required autoComplete="name" />
        <label htmlFor="contact-email">Email</label>
        <input
          id="contact-email"
          name="email"
          type="email"
          required
          autoComplete="email"
        />
        <label htmlFor="contact-message">Message</label>
        <textarea id="contact-message" name="message" required />
        <button type="submit">{pending ? "Sending…" : "Send message"}</button>
      </fieldset>
      <p role="status">{status}</p>
      {error ? <p role="alert">{error}</p> : null}
    </form>
  );
}
```

## Adapting safely

This JSON recipe sends only the listed scalar fields. If a honeypot is configured,
add the matching empty field **and** include it in the JSON payload; do not assume
an undocumented magic name. Match server-side field rules. Use unique IDs if
mounting multiple forms on one page.

For files, replace the JSON body with `fields` and remove `Content-Type` from the
headers (keep `Accept`). Add named file inputs, enable retention/uploads, and
validate 1 MiB per file / five files total. Let the browser set the multipart
boundary. Read [JavaScript/REST](javascript-rest.md) for exact success/error
envelopes and [validation/security/files](validation-security-files.md) for
configuration and privacy.

Do not reset on rejection or automatically retry ambiguous network failures.
Status says **accepted**, not delivered. Native HTML navigation is a different
workflow: use [HTML](html.md) if JavaScript is unnecessary. Server Actions or a
custom proxy are optional application architecture, not required Forms endpoints;
keep private account credentials on the server if one is introduced.

Verify pending/disabled states, keyboard labels, live announcements, preserved
inputs, invalid rules, disabled form, network/JSON failures, and success reset
with mocked fetch. Send a synthetic live test only after permission, including
consent for notifications/forwarding. Do not invent a Forms React package.

Human docs: [React](https://docs.basestack.co/content/forms/guides/react).
