Controlled Forms

In React, form inputs are typically controlled — their value is driven by React state, not the DOM. This means React is always the single source of truth for what's in the input.

The Controlled Input Pattern

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

function ControlledInput() {
  const [value, setValue] = useState('');

  return (
    <div>
      {/* React controls the input value */}
      <input
        type="text"
        value={value}          // driven by state
        onChange={e => setValue(e.target.value)} // update state on change
        placeholder="Type here..."
      />
      <p>You typed: "{value}"</p>
      <p>Length: {value.length}</p>
      <button onClick={() => setValue('')}>Clear</button>
    </div>
  );
}
// The input is "controlled" because React owns its value.
// The DOM is just a display layer.

A Complete Form

The pattern scales to any number of fields:

full-form.jsx
import { useState } from 'react';

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

  // Generic handler works for all text fields
  function handleChange(e) {
    setForm({ ...form, [e.target.name]: e.target.value });
  }

  function handleSubmit(e) {
    e.preventDefault();
    console.log('Submitting:', form);
    // Send to API...
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" value={form.name} onChange={handleChange}
        placeholder="Full name" required />
      <input name="email" type="email" value={form.email} onChange={handleChange}
        placeholder="Email" required />
      <input name="password" type="password" value={form.password} onChange={handleChange}
        placeholder="Password" minLength={8} required />
      <select name="role" value={form.role} onChange={handleChange}>
        <option value="student">Student</option>
        <option value="teacher">Teacher</option>
      </select>
      <button type="submit">Sign Up</button>
    </form>
  );
}

Validation

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

function ValidatedForm() {
  const [email, setEmail] = useState('');
  const [errors, setErrors] = useState({});

  function validate() {
    const newErrors = {};
    if (!email) newErrors.email = 'Email is required';
    else if (!/\S+@\S+\.\S+/.test(email)) newErrors.email = 'Invalid email format';
    return newErrors;
  }

  function handleSubmit(e) {
    e.preventDefault();
    const errs = validate();
    if (Object.keys(errs).length > 0) {
      setErrors(errs);
      return;
    }
    setErrors({});
    alert('Success! Email: ' + email);
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          type="email"
          value={email}
          onChange={e => { setEmail(e.target.value); setErrors({}); }}
          style={{ borderColor: errors.email ? 'red' : undefined }}
          placeholder="your@email.com"
        />
        {errors.email && <span style={{ color: 'red' }}>{errors.email}</span>}
      </div>
      <button type="submit">Submit</button>
    </form>
  );
}

Select, Checkbox, Radio

other-inputs.jsx
import { useState } from 'react';

function AllInputTypes() {
  const [color, setColor] = useState('blue');
  const [agreed, setAgreed] = useState(false);
  const [size, setSize] = useState('medium');

  return (
    <form>
      {/* Select */}
      <select value={color} onChange={e => setColor(e.target.value)}>
        <option value="red">Red</option>
        <option value="blue">Blue</option>
        <option value="green">Green</option>
      </select>
      <p>Selected: {color}</p>

      {/* Checkbox */}
      <label>
        <input type="checkbox" checked={agreed}
          onChange={e => setAgreed(e.target.checked)} />
        I agree to the terms
      </label>
      <p>Agreed: {agreed ? 'Yes' : 'No'}</p>

      {/* Radio buttons */}
      {['small', 'medium', 'large'].map(s => (
        <label key={s}>
          <input type="radio" value={s} checked={size === s}
            onChange={e => setSize(e.target.value)} />
          {s}
        </label>
      ))}
      <p>Size: {size}</p>
    </form>
  );
}
{{callout:tip:The pattern is always the same: value={state} onChange={e => setState(e.target.value)}. Master this and all forms are easy.}}