Free plan250 submissions a month, spam filtering included, no card.See pricing →
Framework · React

React contact form to email, no server

React is a rendering library, not a mail server. A component can collect a name, email, and message, but it has nowhere to deliver them. The usual fix is standing up an API route with an email SDK and spam checks. The lighter one: point your fetch at an endpoint that already does all three.

3 min readAny React appLast reviewed

On this page

At a glance

Component
Client
Backend
None
Submit
fetch, JSON
On error
Keeps what was typed
Endpoint
Public, safe in the browser

The integration

This is a client component. The endpoint URL lives safely in the browser, like a mailto link. Drop it in as-is, or copy just the submit handler into your existing form and keep your own markup and styles.

React
import { useState, type FormEvent } from 'react';

type Status =
  | { state: 'idle' | 'submitting' | 'success' }
  | { state: 'error'; message: string };

export function ContactForm() {
  const [status, setStatus] = useState<Status>({ state: 'idle' });

  async function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const form = event.currentTarget;
    setStatus({ state: 'submitting' });

    try {
      const response = await fetch('https://mtform.co/f/your-form-key', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(Object.fromEntries(new FormData(form))),
      });

      const result = await response.json();
      if (!response.ok) {
        throw new Error(result.error?.message ?? 'Unable to send your message.');
      }

      form.reset();
      setStatus({ state: 'success' });
    } catch (error) {
      // Entered values stay put; we only reset on success.
      const message = error instanceof Error ? error.message : 'Unable to send your message.';
      setStatus({ state: 'error', message });
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="Name" required />
      <input type="email" name="email" placeholder="Email" required />
      <textarea name="message" placeholder="Message" required />

      {/* Spam trap: hidden from people, tempting to bots. */}
      <input
        type="text"
        name="_mtf_honeypot"
        tabIndex={-1}
        autoComplete="off"
        aria-hidden="true"
        style={{ position: 'absolute', left: '-9999px' }}
      />

      <button type="submit" disabled={status.state === 'submitting'}>
        {status.state === 'submitting' ? 'Sending…' : 'Send'}
      </button>

      {status.state === 'success' && <p role="status">Thanks. We got your message.</p>}
      {status.state === 'error' && <p role="alert">{status.message}</p>}
    </form>
  );
}

Step by step

  1. 1

    Create the endpoint

    Create a form in MakeTheForm to get a public endpoint URL and an inbox. The URL is an address, not a secret, so it’s fine in client-side React.

  2. 2

    Wire the submit handler

    In onSubmit, call preventDefault, then fetch() the endpoint with your form data as JSON. Disable the button while the request is in flight.

  3. 3

    Handle success and error

    Treat any 2xx as success and reset the form. On error, show the returned message and leave the entered values in place: never reset on failure.

  4. 4

    Verify and ship

    Click the link in the verification email once, then submit the form. It lands in your inbox and by email with the sender in Reply-To.

Common pitfalls

Resetting the form on error

form.reset() belongs only in the success branch. If you reset in a finally block, a failed send wipes everything the visitor typed. Keep values on error so they can retry without re-entering the whole message.

Reaching for a server you don’t need

A plain React app is client-side, and the endpoint accepts browser POSTs directly. Standing up a Node/Express relay just to forward the request adds a server to deploy and an email service to keep alive: for no gain.

Troubleshooting

SymptomWhat’s happeningFix
CORS errorThe request isn’t a simple JSON POST, or a wrapper added a disallowed header.Send Content-Type: application/json with a plain fetch; the endpoint answers the browser’s CORS preflight.
button stuck disabledThe submitting state never resets because an error threw before it cleared.Set state back to idle/error inside catch, not only on success, so the button re-enables.
422The POST arrived with no fields at all (reserved _mtf_ fields do not count).Give every input a name attribute: the browser never sends an input without one. With fetch, check the body is FormData or JSON built from the form, not empty.

Frequently asked questions

How do I send a React form to email?

Point the form’s submit handler at a hosted form endpoint with fetch(). The endpoint receives the POST, filters spam, and emails you each submission, so you never write or host any mail-sending server code in React.

Does React have built-in form handling?

React manages form state and input in the browser, but has no built-in backend to receive or email submissions. You either build a server route yourself or POST to a form endpoint that handles delivery and storage.

Can I send email directly from React?

Not safely. Sending email needs a credential, and anything in a React bundle is readable by every visitor. Keep the email provider on a server, or POST the form to an endpoint that sends the email for you and needs no key in the browser.

Is it safe to put the form endpoint in React client code?

Yes. The public form key is a non-secret address meant for the browser, like a mailto link. Your destination email never appears in the page source, and there’s no API key to leak.

Other platforms

Keep going

Ship this form for real.

Create an endpoint, paste it in and watch the first submission land in your inbox.

Create free endpoint