React contact form

    A React contact form, twenty lines end to end.

    Design the form once in mailnix, drop a component into your Vite or Create React App project, and POST directly to the public submissions endpoint. No backend, no serverless function, no Nodemailer, no third-party widget in the DOM.

    Start freemailnix Forms

    The job

    Every React contact-form tutorial ranks by selling a service (EmailJS, Formspree) or by walking you through a full-stack build with Node and Nodemailer that is disproportionate to a form that gets three submissions a week. mailnix is the middle path. A publishable form ID in the URL, an origin allowlist as the safety net, and a real inbox behind it with spam scoring, status workflow, and per-submission replies. Works with plain useState + fetch, or with react-hook-form + Zod if you already use those for validation.

    The code

    src/ContactForm.jsx

    import { useState } from "react";
    
    // Replace with the public_id from the mailnix dashboard. Not a secret.
    const FORM_ID = "PUBLIC_ID";
    const ENDPOINT = `https://api.mailnix.ch/v1/public/forms/${FORM_ID}`;
    
    export default function ContactForm() {
      const [status, setStatus] = useState("idle");
      const [error, setError] = useState(null);
    
      async function onSubmit(e) {
        e.preventDefault();
        setStatus("sending");
        setError(null);
    
        const def = await fetch(ENDPOINT).then((r) => r.json());
        const data = Object.fromEntries(new FormData(e.currentTarget));
    
        try {
          const res = await fetch(`${ENDPOINT}/submissions`, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({
              data: { ...data, _mnx_ft: def.fill_time_token },
            }),
          });
          if (res.status !== 201) {
            const body = await res.json().catch(() => ({}));
            throw new Error(body.error || "Submission failed");
          }
          setStatus("sent");
          e.target.reset();
        } catch (err) {
          setStatus("error");
          setError(err.message);
        }
      }
    
      if (status === "sent") {
        return <p>Thanks. We got your message and will be in touch.</p>;
      }
    
      return (
        <form onSubmit={onSubmit} noValidate>
          <label>
            Name
            <input name="name" required autoComplete="name" />
          </label>
          <label>
            Email
            <input name="email" type="email" required autoComplete="email" />
          </label>
          <label>
            Message
            <textarea name="message" required rows={5} />
          </label>
          <button type="submit" disabled={status === "sending"}>
            {status === "sending" ? "Sending..." : "Send message"}
          </button>
          {status === "error" && <p role="alert">Something went wrong: {error}</p>}
        </form>
      );
    }

    Setup, in order

    1. Create a form in the mailnix dashboard and note the public_id shown next to it.
    2. In the form's settings, add your dev origin (http://localhost:5173 for Vite, http://localhost:3000 for CRA) and your production origin to Allowed origins.
    3. Add and verify at least one owner notify address so every submission emails you.
    4. Drop the component above into your React project. The useState machine covers idle, sending, sent, and error states with room for a react-hook-form variant on top.
    5. Submit once yourself and confirm the entry lands in the mailnix Forms inbox with a plausible spam score, then remove any mailto: fallback link you still had.

    What you get on top

    A real inbox, not just email forwarding

    EmailJS and Formspree forward the fields and stop. mailnix Forms keeps every submission in an inbox with spam scoring, status workflow, CSV export, retention window, and one-click replies from the dashboard.

    Origin lock, not just an obscured key

    The public_id is visible in your bundle by design. The security boundary is the Origin allowlist plus per-form rate caps, so a copy of the id on another domain gets rejected server-side.

    Works with react-hook-form and Zod

    The JSON body shape is the only contract. Use react-hook-form for validation and let mailnix Forms enforce the schema server-side; both stay in sync because both are generated from the field list you designed once.

    MCP-friendly for AI-built apps

    The same form is reachable from Claude, ChatGPT, and Cursor over MCP. form_reply_send lets an assistant answer a submission from the chat, always to the captured address.

    Gotchas people actually hit

    • onSubmit must call e.preventDefault(). Without it the browser does a full-page GET or POST navigation and the fetch never runs.
    • Only publishable-style ids belong in the browser bundle. Never ship an mnx_live_ transactional token in a React component.
    • Origin lock in the mailnix dashboard has to include BOTH your dev origin (http://localhost:5173 or :3000) AND your production domain, otherwise CORS blocks the request from one of them.
    • Disable the submit button while status === 'sending'. Without it a fast double-click posts twice and the inbox will show two identical entries.
    • React 18 StrictMode mounts components twice in dev. Keep the fetch in the submit handler, not in a useEffect keyed to mount.
    • Capture e.currentTarget synchronously (as FormData in the snippet). React pools the event, so reading e.target after an await is undefined behaviour.

    React contact form FAQ

    Do I need a backend to handle the form?

    No. A public form endpoint posts directly from the browser to api.mailnix.ch. The origin allowlist and per-form rate caps are what a backend would otherwise give you.

    Is the public form id a secret?

    It is not. It is designed to appear in your React bundle and page source. Only tokens starting with mnx_live_ are secrets and must stay on a server.

    Can I use react-hook-form with mailnix?

    Yes. react-hook-form handles validation and gives you a values object; call fetch to the mailnix submit endpoint from its onSubmit. mailnix does not care which library assembled the payload.

    How do I get notified when someone submits the form?

    Add a verified owner notify address to the form in the mailnix dashboard. Every submission emails that address, and the raw submission is also visible in the inbox with a spam score.

    How do I stop spam?

    Every submission is scored on ingest and low-quality entries land in a separate Spam view. Add the honeypot input at any time, or turn on Turnstile or hCaptcha in the form's settings.

    Why do I get a CORS error on localhost?

    The form's Origin allowlist does not include your dev origin. Add http://localhost:5173 (Vite) or http://localhost:3000 (CRA) alongside your production domain.

    One HTTP call to get started

    Put a form on your site tonight. Wire your provider when you're ready.