React Forms

Controlled inputs keep an input element's value in sync with React state, so state is always the single source of truth for what's on screen.

Controlled Inputs with value and onChange

A controlled input sets its value from state and updates that state through onChange on every keystroke. Because the input's displayed value always comes from state, React can validate, transform, or reset the value at any time — the DOM never holds a value React doesn't know about.

A Single Controlled Text Input

import { useState } from 'react';

function NameInput() {
  const [name, setName] = useState('');

  return (
    <input
      type="text"
      value={name}
      onChange={(event) => setName(event.target.value)}
    />
  );
}

export default NameInput;

Handling Multiple Fields with One Handler

Rather than writing a separate handler for every field, give each input a name attribute matching a key in a single state object, then use one handleChange function that reads event.target.name to know which key to update. This scales cleanly as forms grow past two or three fields.

One Handler, Many Fields

import { useState } from 'react';

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

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

  return (
    <form>
      <input name="email" value={form.email} onChange={handleChange} />
      <input
        name="password"
        type="password"
        value={form.password}
        onChange={handleChange}
      />
    </form>
  );
}

export default SignupForm;
Note: The computed property syntax [name]: value is what makes the shared handler work — it dynamically sets whichever key matches the input's name attribute, whether that's 'email', 'password', or any field you add later.

Submitting Forms and preventDefault

By default, submitting an HTML form reloads the page — the browser sends a request and throws away all your JavaScript state. Calling event.preventDefault() inside your onSubmit handler stops that default navigation so you can handle the submitted data entirely in React instead.

A Complete Submit Flow

import { useState } from 'react';

function ContactForm() {
  const [message, setMessage] = useState('');
  const [sent, setSent] = useState(false);

  function handleSubmit(event) {
    event.preventDefault();
    console.log('Sending:', message);
    setSent(true);
  }

  if (sent) {
    return <p>Thanks — your message was sent!</p>;
  }

  return (
    <form onSubmit={handleSubmit}>
      <textarea
        value={message}
        onChange={(event) => setMessage(event.target.value)}
      />
      <button type="submit">Send</button>
    </form>
  );
}

export default ContactForm;
Input TypeState ShapeNotes
text / textareastringRead event.target.value
checkboxbooleanRead event.target.checked, not .value
selectstring (the selected option's value)Behaves like a text input
radio groupstring matching the checked option's valueShare one state key across all radios in the group
  • Always pair value with onChange — a value prop without onChange makes React warn about a read-only field.
  • Call event.preventDefault() as the first line of your onSubmit handler.
  • Use a single object in state with computed [name]: value updates once a form has more than two or three fields.
  • Disable or hide the submit button while an async submission is in flight to prevent duplicate submits.

Exercise: React Forms

In a controlled input, what makes React the 'single source of truth' for the field's value?