Formclick

How to Integrate HTML Forms With React

Build forms in React the simple way — uncontrolled inputs with FormData, controlled inputs when you need them, loading and error states, and submitting to a form backend with no server.

FFormclick Team3 min read
Close-up of HTML and JavaScript code on a computer screen in Visual Studio Code.
Photo by Antonio Batinić on Pexels
On this page
  1. Approach 1: Uncontrolled form + FormData (recommended for most forms)
  2. Approach 2: Controlled inputs (when the UI depends on values)
  3. Approach 3: React 19 form actions
  4. Keep the access key in config
  5. Validation: when to add a library
  6. Accessibility checklist for React forms

React developers often over-engineer forms: a useState for every field, an onChange handler for every input, and a validation library for a three-field contact form. Modern React — and plain HTML — can do much of this for you.

This guide shows three approaches, from simplest to most flexible, all submitting to a form backend so you don't need your own server.

Let the DOM hold the values. Read them all at once on submit with FormData.

import { useState } from 'react';

export default function ContactForm() {
  const [status, setStatus] = useState('idle'); // idle | sending | sent | error

  async function handleSubmit(event) {
    event.preventDefault();
    const form = event.currentTarget;
    setStatus('sending');

    try {
      const response = await fetch('https://api.formclick.io/submit', {
        method: 'POST',
        headers: { Accept: 'application/json' },
        body: new FormData(form),
      });
      const result = await response.json();
      if (!response.ok || !result.success) throw new Error(result.message);
      form.reset();
      setStatus('sent');
    } catch {
      setStatus('error');
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />
      <input type="checkbox" name="botcheck" style={{ display: 'none' }} tabIndex={-1} />

      <label htmlFor="name">Name</label>
      <input id="name" name="name" autoComplete="name" required />

      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" autoComplete="email" required />

      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" rows={5} required />

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

      <p role="status" aria-live="polite">
        {status === 'sent' && 'Thanks! We will reply soon.'}
        {status === 'error' && 'Something went wrong. Please try again.'}
      </p>
    </form>
  );
}

Why this is a good default:

  • No state per field — fewer re-renders and far less code.
  • Native validation still works — required and type="email" run before onSubmit fires.
  • Files just work — FormData includes file inputs automatically.
  • Remember React's attribute names: htmlFor, autoComplete, tabIndex.

Approach 2: Controlled inputs (when the UI depends on values)

Use controlled inputs when you need to react to values as they change — live character counters, dependent fields, instant formatting.

import { useState } from 'react';

const MAX = 500;

export default function FeedbackForm() {
  const [message, setMessage] = useState('');
  const [rating, setRating] = useState('');

  return (
    <form action="https://api.formclick.io/submit" method="POST">
      <input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />

      <label htmlFor="rating">Rating</label>
      <select id="rating" name="rating" value={rating}
              onChange={(e) => setRating(e.target.value)} required>
        <option value="">Choose…</option>
        <option value="5">Excellent</option>
        <option value="3">OK</option>
        <option value="1">Poor</option>
      </select>

      {rating === '1' && (
        <>
          <label htmlFor="issue">What went wrong?</label>
          <input id="issue" name="issue" required />
        </>
      )}

      <label htmlFor="message">Comments</label>
      <textarea id="message" name="message" maxLength={MAX}
                value={message} onChange={(e) => setMessage(e.target.value)} />
      <small>{MAX - message.length} characters left</small>

      <button type="submit">Send feedback</button>
    </form>
  );
}

Notice this form uses a plain action — it submits normally, with React only managing the dynamic parts.

Approach 3: React 19 form actions

React 19 lets you pass a function to a form's action prop. Combined with useActionState, it handles pending state for you:

import { useActionState } from 'react';

async function sendMessage(previousState, formData) {
  const response = await fetch('https://api.formclick.io/submit', {
    method: 'POST',
    headers: { Accept: 'application/json' },
    body: formData,
  });
  const result = await response.json();
  return result.success
    ? { ok: true, message: 'Thanks! Message sent.' }
    : { ok: false, message: result.message || 'Please try again.' };
}

export default function ContactForm() {
  const [state, formAction, isPending] = useActionState(sendMessage, null);

  return (
    <form action={formAction}>
      <input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />
      <input name="email" type="email" required aria-label="Email" />
      <textarea name="message" required aria-label="Message" />
      <button disabled={isPending}>{isPending ? 'Sending…' : 'Send'}</button>
      {state && <p role="status">{state.message}</p>}
    </form>
  );
}

React resets uncontrolled fields automatically after a successful action.

Keep the access key in config

Store the key in an environment variable so different environments can use different forms:

// Vite:     import.meta.env.VITE_FORMCLICK_KEY
// Next.js:  process.env.NEXT_PUBLIC_FORMCLICK_KEY
<input type="hidden" name="access_key" value={import.meta.env.VITE_FORMCLICK_KEY} />

It's safe for this key to reach the browser: a Formclick access key can only submit to its own form, not read submissions or settings.

Validation: when to add a library

  • Contact, feedback, sign-up forms — native HTML validation is enough.
  • Complex forms with cross-field rules, arrays of fields or multi-step flows — consider React Hook Form with a schema library such as Zod.

Accessibility checklist for React forms

  • Every input has a <label htmlFor> or an aria-label.
  • Status messages live in an element with role="status".
  • Focus moves to the first invalid field or the success message after submit.
  • Buttons are real <button> elements with clear text.

Not using React? The same pattern in vanilla JS is in How to Submit HTML Forms Using the Fetch API. Ready-made designs are in our HTML form templates.

Found this useful? Share it.

All articles