Add a Contact Form to React Without a Backend

Build a React contact form component that sends emails, stores submissions, and filters spam — without writing a single line of server-side code.

· 7 min read

In this guide

Why you don't need a backend for React forms

The typical advice for handling contact forms in React goes like this: spin up an Express server, configure Nodemailer with SMTP credentials, write a POST endpoint, handle CORS, deploy it somewhere, and keep it running. That's a whole backend for a single form.

If you're building a portfolio, a SaaS landing page, or any React app that doesn't already have a server, there's no reason to create one just for a contact form. A form backend service handles the server-side work for you — you make a fetch call from your React component and the service takes care of email delivery, spam filtering, and submission storage.

Optaristo is a form backend built for exactly this. It's free during beta, has no submission limits, and works with any React setup — Create React App, Vite, Gatsby, or a custom config.

Set up Optaristo

  1. Create a free account — no credit card needed.
  2. Click "Create Form" in the dashboard and name it (e.g., "Portfolio Contact").
  3. Copy your access key. You'll use this in your React component.

Build the ContactForm component

Here's a complete, production-ready contact form component. It uses controlled inputs, handles loading and success states, and submits to Optaristo via JSON:

ContactForm.jsx
import { useState } from 'react';

export default function ContactForm() {
  const [status, setStatus] = useState('idle');
  const [form, setForm] = useState({
    name: '',
    email: '',
    message: ''
  });

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('loading');

    const res = await fetch(
      'https://app.optaristo.com/api/submit',
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json'
        },
        body: JSON.stringify({
          access_key: 'YOUR_ACCESS_KEY',
          ...form
        })
      }
    );

    if (res.ok) {
      setStatus('success');
      setForm({ name: '', email: '', message: '' });
    } else {
      setStatus('error');
    }
  }

  if (status === 'success') {
    return <p>Thanks! We'll be in touch.</p>;
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        placeholder="Name"
        value={form.name}
        onChange={e => setForm({...{form, name: e.target.value})}
        required
      />
      <input
        type="email"
        placeholder="Email"
        value={form.email}
        onChange={e => setForm({...{form, email: e.target.value})}
        required
      />
      <textarea
        placeholder="Message"
        value={form.message}
        onChange={e => setForm({...{form, message: e.target.value})}
        rows={5}
        required
      />
      <button type="submit" disabled={status === 'loading'}>
        {status === 'loading' ? 'Sending...' : 'Send Message'}
      </button>
      {status === 'error' &&
        <p>Something went wrong. Try again.</p>}
    </form>
  );
}

Replace YOUR_ACCESS_KEY with the key from your Optaristo dashboard. That's literally all you need — import this component wherever you want a contact form.

Loading and success states

The component above already handles three states:

No external state libraries needed. React's built-in useState handles the entire flow cleanly.

Client-side validation

The required attribute on each input handles basic validation natively — the browser won't let the form submit with empty fields, and type="email" enforces a valid email format.

For most contact forms, that's sufficient. If you need more advanced validation (minimum message length, phone number formatting), add checks inside handleSubmit before the fetch call. But don't overengineer it — a contact form is not a checkout flow.

Spam protection

Optaristo rate-limits submissions per IP automatically. For an extra layer of protection, add a honeypot field — a hidden input that bots fill in but humans never see:

// Add to the form body in your JSX:
<input
  type="hidden"
  name="_gotcha"
  style={{ display: 'none' }}
  tabIndex={-1}
  autoComplete="off"
/>

No reCAPTCHA, no annoying puzzles. Bots fill hidden fields automatically; real visitors never see it.

TypeScript version

If you're using TypeScript, here's the same component with proper types:

ContactForm.tsx
import { useState, FormEvent, ChangeEvent } from 'react';

type Status = 'idle' | 'loading' | 'success' | 'error';

export default function ContactForm() {
  const [status, setStatus] = useState<Status>('idle');
  const [form, setForm] = useState({
    name: '', email: '', message: ''
  });

  async function handleSubmit(e: FormEvent) {
    e.preventDefault();
    setStatus('loading');
    const res = await fetch('https://app.optaristo.com/api/submit', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        access_key: 'YOUR_ACCESS_KEY', ...form
      })
    });
    setStatus(res.ok ? 'success' : 'error');
  }

  function update(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
    setForm({ ...form, [e.target.name]: e.target.value });
  }

  // ... same JSX as above
}

Frequently asked questions

Do I need a backend server for a React contact form?

No. With a form backend service like Optaristo, your React component sends form data directly to an API endpoint. The service handles email delivery, spam filtering, and submission storage — no Express, no API routes required.

Can I use Optaristo with Create React App or Vite?

Yes. Optaristo works with any React setup — Create React App, Vite, Remix, Gatsby, or a custom webpack config. You just make a fetch call from your component.

Does a React form backend handle CORS?

Yes. Optaristo's API accepts cross-origin requests, so you won't run into CORS issues when submitting forms from your React app regardless of where it's hosted.

Skip the backend. Ship the form.

Get your access key, drop in the component, and start receiving submissions in under a minute.

Get my free access key

Related guides