Skip to content
React rc forms 5 min read

Forms in React

Forms are where a React app stops being read-only and starts collecting real input—logins, searches, checkouts, settings. React does not ship a <Form> component; instead it gives you plain HTML form elements plus two tools for wiring them up: state for tracking values and event handlers for reacting to changes and submission. React 19 adds a higher-level Actions model on top of that foundation, so it is worth understanding both the classic approach and the modern direction before you build anything serious. This page sets up the rest of the Forms section.

The two ways to manage form data

Every input in React is either controlled or uncontrolled. A controlled input binds its value to state and updates that state on every keystroke, making React the single source of truth. An uncontrolled input lets the DOM keep the value and you read it only when you need it, usually via a ref or at submit time.

ApproachValue lives inRead withBest for
ControlledReact stateThe state variableLive validation, dependent fields, instant feedback
UncontrolledThe DOMA ref or FormDataSimple forms, file inputs, less re-rendering
Form ActionsSubmitted FormDataAction function argumentServer mutations, progressive enhancement

Most production forms are controlled because it makes the data trivial to validate and transform. The controlled pattern is covered in depth on its own page; here is the shape it takes.

import { useState } from "react";

function EmailField() {
  const [email, setEmail] = useState("");

  return (
    <input
      type="email"
      value={email}
      onChange={(e) => setEmail(e.target.value)}
    />
  );
}

Handling submission

Forms submit through the native submit event, which React surfaces as onSubmit on the <form> element. By default a submit reloads the page—the old server round-trip behavior. In a single-page React app you almost always call event.preventDefault() to stop that and handle the data in JavaScript instead.

import { useState } from "react";

function LoginForm() {
  const [form, setForm] = useState({ email: "", password: "" });

  function handleChange(e) {
    const { name, value } = e.target;
    setForm((prev) => ({ ...prev, [name]: value }));
  }

  async function handleSubmit(e) {
    e.preventDefault();
    const res = await fetch("/api/login", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(form),
    });
    console.log("Logged in:", res.ok);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" value={form.email} onChange={handleChange} />
      <input name="password" type="password" value={form.password} onChange={handleChange} />
      <button type="submit">Log in</button>
    </form>
  );
}

Always put preventDefault() first in the handler. Forgetting it lets the browser navigate away mid-request, and the bug looks like a flickering page rather than an obvious error.

A <button type="submit"> (the default for buttons inside a form) triggers onSubmit, and so does pressing Enter in a text field. That keyboard behavior is free accessibility you get from using a real <form>—don’t replace it with a click handler on a <div>.

Gathering values without per-field state

When you don’t need live validation, the FormData API reads every named field at submit time without any useState. This keeps inputs uncontrolled and the component lean.

function NewsletterForm() {
  function handleSubmit(e) {
    e.preventDefault();
    const data = new FormData(e.target);
    console.log(Object.fromEntries(data));
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" />
      <input name="topic" defaultValue="weekly" />
      <button type="submit">Subscribe</button>
    </form>
  );
}

Output:

{ email: "[email protected]", topic: "weekly" }

FormData keys come from each input’s name attribute, so name your fields even when they are uncontrolled.

Client-side validation

The fastest validation is the browser’s own: required, type="email", minLength, and pattern attributes block submission and show native messages with zero JavaScript. For richer rules you validate in state and render error messages yourself.

import { useState } from "react";

function SignupForm() {
  const [email, setEmail] = useState("");
  const [error, setError] = useState("");

  function handleSubmit(e) {
    e.preventDefault();
    if (!email.includes("@")) {
      setError("Please enter a valid email address.");
      return;
    }
    setError("");
    console.log("Submitting", email);
  }

  return (
    <form onSubmit={handleSubmit} noValidate>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      {error && <p role="alert">{error}</p>}
      <button type="submit">Continue</button>
    </form>
  );
}

Validate on submit for a calm first impression, then optionally re-validate on change once a field has been touched. The dedicated validation page goes deeper, and form libraries handle the bookkeeping for large forms.

The modern Actions model

React 19 lets you pass a function directly to a form’s action prop. When the form submits, React calls that function with the FormData, manages pending state, and resets the form on success—no manual preventDefault or onChange wiring. Pairing it with useActionState gives you the result and a pending flag in one hook.

import { useActionState } from "react";

async function subscribe(prevState, formData) {
  const email = formData.get("email");
  await fetch("/api/subscribe", { method: "POST", body: formData });
  return { message: `Subscribed ${email}` };
}

function Subscribe() {
  const [state, formAction, isPending] = useActionState(subscribe, { message: "" });

  return (
    <form action={formAction}>
      <input name="email" type="email" />
      <button disabled={isPending}>{isPending ? "Saving..." : "Subscribe"}</button>
      {state.message && <p>{state.message}</p>}
    </form>
  );
}

This is the direction React is moving: less manual plumbing, built-in pending and error handling, and forms that work even before JavaScript loads. The Form Actions page covers useActionState, useFormStatus, and optimistic updates in detail.

Best Practices

  • Use a real <form> element so Enter-to-submit and submit buttons work for keyboard and screen-reader users.
  • Call event.preventDefault() before any async work in classic onSubmit handlers.
  • Reach for controlled inputs when you need live feedback; use FormData or refs for simple, write-only forms.
  • Let native attributes (required, type, pattern) do the easy validation before adding custom logic.
  • Disable the submit button while a request is in flight to prevent duplicate submissions.
  • Prefer the React 19 action prop and useActionState for new forms that mutate server data.
Last updated June 14, 2026
Was this helpful?