React Forms and Controlled Inputs: Text, Checkbox, Select and Submit

React.js Full Course: Beginner to Advanced · Start Here · 10 min read · Updated

A controlled input

  1. The person types a letter in the field.
  2. The onChange handler runs and reads event.target.value.
  3. The handler saves the text in state with the setter.
  4. React draws again, and the input shows the value from state.
  5. State is the single source of truth, so the screen and the data can never disagree.

Forms are how people give data to your app: they sign up, search, write comments and pay. In plain HTML the browser keeps the text typed in each input. In React we usually keep it in state instead, so the screen and the data always agree. This lesson teaches the most common way, called controlled inputs.

Controlled input: the idea

A controlled input gets its value from React state, and tells React about every change. Two props make this work: value (what to show) and onChange (what to do when the person types).

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

  return (
    <form>
      <label htmlFor="name">Your name</label>
      <input
        id="name"
        value={name}
        onChange={(event) => setName(event.target.value)}
      />
      <p>You typed: {name}</p>
    </form>
  );
}

Follow the loop: the person types a letter, onChange runs, setName saves the new text in state, React draws again, and the input shows the new value. The state is the single source of truth. If you set value but forget onChange, the input becomes read-only and React warns you.

Do you see the label with htmlFor? It connects the text to the input. Clicking the label focuses the input, and screen readers read the label out. Always add one. A placeholder is not a replacement for a label.

Many fields, one state object

A form with ten fields should not need ten useState calls and ten handlers. Use one object, and give each input a name. One handler can then update any field:

function SignUpForm() {
  const [form, setForm] = useState({ name: '', email: '', country: 'India' });

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

  function handleSubmit(event) {
    event.preventDefault();
    console.log('Form data:', form);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" value={form.name} onChange={handleChange} placeholder="Name" />
      <input name="email" type="email" value={form.email} onChange={handleChange} placeholder="Email" />
      <button type="submit">Sign up</button>
    </form>
  );
}

The strange [name]: value is a computed property name. It uses the value of the variable name (such as 'email') as the key. And ...form copies all the old fields first, so only one field is replaced. Remember the rule: never change state directly.

Checkboxes

A checkbox uses checked instead of value, and you read event.target.checked:

const [agree, setAgree] = useState(false);

<label>
  <input type="checkbox" checked={agree} onChange={(e) => setAgree(e.target.checked)} />
  I accept the terms
</label>
<button disabled={!agree}>Continue</button>

The disabled prop is tied to state, so the button works only after the box is ticked. This is a small taste of how state controls the page.

Select (dropdown)

In React a select also uses value on the select element itself, not selected on each option:

<select value={country} onChange={(e) => setCountry(e.target.value)}>
  <option value="India">India</option>
  <option value="UK">United Kingdom</option>
  <option value="Canada">Canada</option>
</select>

Radio buttons

Radio buttons share one state value. Each radio is checked when its own value equals the state:

const [plan, setPlan] = useState('free');

{['free', 'pro'].map((p) => (
  <label key={p}>
    <input type="radio" name="plan" value={p} checked={plan === p} onChange={() => setPlan(p)} />
    {p}
  </label>
))}

Textarea

In HTML, a textarea has its text between tags. In React it is like an input: use value and onChange, and write it as a self-closing tag or with value:

<textarea value={message} onChange={(e) => setMessage(e.target.value)} rows={4} />

Simple validation

Validation means checking that what the person typed makes sense. Check on submit, and show a clear message next to the field. Keep the errors in state:

function ContactForm() {
  const [email, setEmail] = useState('');
  const [error, setError] = useState('');

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

  return (
    <form onSubmit={handleSubmit} noValidate>
      <label htmlFor="email">Email</label>
      <input id="email" value={email} onChange={(e) => setEmail(e.target.value)}
        aria-invalid={error ? 'true' : 'false'} aria-describedby="email-error" />
      {error && <p id="email-error" role="alert">{error}</p>}
      <button type="submit">Send</button>
    </form>
  );
}
  • aria-invalid and aria-describedby help screen readers know that the field has a problem and what it is.
  • role="alert" makes screen readers read the message when it appears.
  • Write messages that help: say what to do, not only what went wrong.
  • Always validate again on the server. The browser can be bypassed, so client checks are only a convenience.

Clearing the form after submit

Because the form lives in state, clearing it is easy: set the state back to the starting value. Keep that value in one constant so you can reuse it.

const empty = { name: '', email: '' };
const [form, setForm] = useState(empty);

function handleSubmit(event) {
  event.preventDefault();
  save(form);
  setForm(empty);
}

Controlled or uncontrolled?

There is another way: uncontrolled inputs, where the browser keeps the value and you read it only when needed, with a ref or with FormData. It is shorter for simple forms and for very large forms. Lesson 19 shows it. For most forms where you check the text as people type, controlled inputs are the easiest to understand.

Common mistakes

  • Setting value without onChange: the field becomes impossible to type in.
  • Starting with undefined: useState() with no value makes the input uncontrolled, then controlled, and React warns. Start with ''.
  • Changing the form object directly instead of copying it.
  • Using onClick on the submit button instead of onSubmit on the form.
  • Forgetting the label, which hurts people who use screen readers.

Walk through a keystroke

Controlled inputs are easier to trust when you follow one key press from start to finish. Say the person types the letter “A” in an empty name field.

  1. The browser fires a change event on the input.
  2. React calls your onChange handler with the event. event.target.value is now A.
  3. Your handler calls setName('A'). React stores the new value and plans a draw.
  4. React calls your component again. This time name is A.
  5. The input is drawn with value={name}, so it shows A. Everything that uses name, such as a counter of characters, shows the new value too.

This happens fast enough that you never notice it. The key idea is that the input never keeps its own text. It always shows what is in state. That is why the state and the screen cannot disagree, and why you can do things like change the text to capitals as people type, limit the length, or clear the field from code.

A small example: capital letters and a limit

<input
  value={code}
  onChange={(e) => setCode(e.target.value.toUpperCase().slice(0, 6))}
  placeholder="Enter a six letter code"
/>

The text is changed before it is saved in state, so the field always shows capital letters and never more than six of them. With a normal uncontrolled input you would need extra code to do this. Because the input is controlled, it is one line.

Questions beginners often ask

Do I need a library for forms? Not for small forms. Controlled inputs and a simple validation function are enough. Look at a library when your forms are large, have many steps or have many rules.

Why does my number input give text? The value of every input is text, even for type="number". Convert it with Number(value) when you need a number, and think about what to do when the field is empty.

How do I show a password? Keep a true or false state, such as show, and set type={show ? "text" : "password"}. Add a button with a clear label like “Show password”, and tell screen readers its state with aria-pressed.

How do I stop people from sending a form twice? Keep a sending state. Set it to true at the start of submit, disable the button while it is true, and set it to false when the request ends, whether it worked or not.

Your task

  1. Build a sign-up form with name, email, a country dropdown and a checkbox “I agree”. Use one state object and one change handler.
  2. Disable the Submit button until the box is ticked.
  3. On submit, check that the name is not empty and the email has an @. Show an error message next to the field.
  4. After a good submit, show “Thanks, NAME!” and clear the form.

How to check it worked

  • Every field can be typed in, and the page shows the changes at once.
  • Errors appear next to the field and disappear after a correct submit.
  • You can reach every field and the button using only the Tab key.

Summary

  • A controlled input has value from state and onChange that updates state.
  • Use one state object and [name]: value to handle many fields with one function.
  • Checkboxes use checked, selects use value on the select, and radios compare their value to state.
  • Validate on submit with clear messages, add labels, and check again on the server.

Frequently asked questions

What is a controlled input?

An input whose value comes from React state and whose changes are saved to state through onChange. The state is the single source of truth.

Why can I not type in my input?

You probably set value without onChange. The input shows the fixed value from state. Add an onChange that updates the state.

Should I use a form library?

For small forms, state is enough. For large forms with many rules, a library like React Hook Form can save time. Learn the basics first.

Why does React warn about controlled and uncontrolled inputs?

The value started as undefined and later became a string. Start with an empty string so the input is controlled from the start.