Episode 2 — React Frontend Architecture NextJS / 2.5 — Event Handling and Conditional Rendering

2.5.b — Controlled Inputs

In one sentence: In React, controlled inputs bind a form element's value directly to component state via value + onChange, giving you complete programmatic control over every keystroke, validation rule, and formatting decision.

Navigation: ← Handling Click Events · Next → Conditional Rendering Patterns


Table of Contents

  1. What is a Controlled Input?
  2. Uncontrolled vs Controlled — The Mental Model
  3. Controlled Text Input
  4. The onChange Event in Detail
  5. Controlled Textarea
  6. Controlled Select (Dropdown)
  7. Controlled Checkbox
  8. Controlled Radio Buttons
  9. Controlled Number Input
  10. Controlled Range (Slider)
  11. Controlled File Input — The Exception
  12. Multi-Field Forms with a Single Handler
  13. Input Formatting and Masking
  14. Real-Time Validation
  15. Common Mistakes and Anti-Patterns
  16. When to Use Uncontrolled Instead
  17. Key Takeaways

1. What is a Controlled Input?

A controlled input is a form element whose value is driven by React state. The component is the "single source of truth" — the DOM input merely reflects what state says.

┌──────────────────────────────────────────────────┐
│                   React Component                 │
│                                                    │
│   state: { name: "Alice" }                        │
│      │                                             │
│      │ value={name}     ┌──────────────────┐      │
│      └────────────────► │  <input>          │      │
│                          │  displays "Alice" │      │
│      ┌──────────────── │  user types "B"   │      │
│      │ onChange(e)       └──────────────────┘      │
│      │                                             │
│      ▼                                             │
│   setName(e.target.value)                         │
│   state: { name: "AliceB" }                       │
│      │                                             │
│      │ value={name}                                │
│      └──► input now shows "AliceB"                │
│                                                    │
└──────────────────────────────────────────────────┘

The loop:

  1. State holds the value
  2. Input displays the value
  3. User types → onChange fires
  4. Handler calls setState with new value
  5. React re-renders → input shows updated value
  6. Go to step 3

2. Uncontrolled vs Controlled — The Mental Model

Uncontrolled Input (DOM is the source of truth)

function UncontrolledForm() {
  const inputRef = useRef(null);

  function handleSubmit(event) {
    event.preventDefault();
    // Read value directly from DOM
    console.log(inputRef.current.value);
  }

  return (
    <form onSubmit={handleSubmit}>
      {/* DOM manages its own state */}
      <input ref={inputRef} defaultValue="initial" />
      <button type="submit">Submit</button>
    </form>
  );
}

Controlled Input (React state is the source of truth)

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

  function handleSubmit(event) {
    event.preventDefault();
    // Value is already in state
    console.log(value);
  }

  return (
    <form onSubmit={handleSubmit}>
      {/* React controls the value */}
      <input value={value} onChange={(e) => setValue(e.target.value)} />
      <button type="submit">Submit</button>
    </form>
  );
}

Side-by-Side Comparison

AspectUncontrolledControlled
Source of truthDOMReact state
Access valueref.current.valuestate variable
Set initial valuedefaultValueuseState('initial')
Real-time validationHardEasy
Conditional disableHarddisabled={condition}
Format as you typeVery hardEasy
Instant feedbackManual DOM queryAutomatic re-render
Reset formref.current.value = ''setValue('')
When to useFile inputs, simple one-offsAlmost everything else

3. Controlled Text Input

Basic Pattern

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

  return (
    <div>
      <label htmlFor="name">Name:</label>
      <input
        id="name"
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Enter your name"
      />
      <p>Hello, {name || 'stranger'}!</p>
    </div>
  );
}

With Character Limit

function LimitedInput({ maxLength = 50 }) {
  const [text, setText] = useState('');

  function handleChange(event) {
    const newValue = event.target.value;
    if (newValue.length <= maxLength) {
      setText(newValue);
    }
    // If over limit, simply don't update — input stays the same
  }

  return (
    <div>
      <input
        type="text"
        value={text}
        onChange={handleChange}
        placeholder={`Max ${maxLength} characters`}
      />
      <span style={{ color: text.length > maxLength * 0.8 ? 'red' : '#999' }}>
        {text.length}/{maxLength}
      </span>
    </div>
  );
}

Uppercase-Only Input

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

  function handleChange(event) {
    // Transform on every keystroke
    setValue(event.target.value.toUpperCase());
  }

  return <input value={value} onChange={handleChange} placeholder="AUTO UPPERCASE" />;
}

4. The onChange Event in Detail

What onChange Actually Fires For

In HTML, change fires when the element loses focus. In React, onChange fires on every keystroke (like the native input event). This is a React-specific behavior.

function ChangeVsInput() {
  return (
    <div>
      {/* React onChange fires on EVERY keystroke */}
      <input
        onChange={(e) => console.log('React onChange:', e.target.value)}
      />

      {/* To get native "change" behavior (on blur), use onBlur */}
    </div>
  );
}

The Event Object for Inputs

function InputEventDetails() {
  function handleChange(event) {
    console.log(event.target.value);      // Current input value
    console.log(event.target.name);       // Input's "name" attribute
    console.log(event.target.type);       // "text", "email", etc.
    console.log(event.target.checked);    // For checkboxes
    console.log(event.target.selectedOptions); // For selects (multiple)
  }

  return <input name="email" type="email" onChange={handleChange} />;
}

5. Controlled Textarea

In HTML, textarea content goes between tags: <textarea>Hello</textarea>. In React, textarea uses value prop just like an input.

function ControlledTextarea() {
  const [bio, setBio] = useState('');

  return (
    <div>
      <label htmlFor="bio">Bio:</label>
      <textarea
        id="bio"
        value={bio}
        onChange={(e) => setBio(e.target.value)}
        rows={5}
        cols={40}
        placeholder="Tell us about yourself..."
        style={{ resize: 'vertical' }}
      />
      <p>{bio.length} characters | ~{Math.ceil(bio.split(/\s+/).filter(Boolean).length)} words</p>
    </div>
  );
}

Auto-Resizing Textarea

function AutoResizeTextarea() {
  const [value, setValue] = useState('');
  const textareaRef = useRef(null);

  function handleChange(event) {
    setValue(event.target.value);
    
    // Auto-resize
    const textarea = textareaRef.current;
    textarea.style.height = 'auto';
    textarea.style.height = textarea.scrollHeight + 'px';
  }

  return (
    <textarea
      ref={textareaRef}
      value={value}
      onChange={handleChange}
      style={{
        width: '100%',
        minHeight: 40,
        overflow: 'hidden',
        resize: 'none',
      }}
      placeholder="Type and I'll grow..."
    />
  );
}

6. Controlled Select (Dropdown)

Single Select

function CountrySelect() {
  const [country, setCountry] = useState('');

  const countries = [
    { code: 'us', name: 'United States' },
    { code: 'uk', name: 'United Kingdom' },
    { code: 'in', name: 'India' },
    { code: 'de', name: 'Germany' },
    { code: 'jp', name: 'Japan' },
  ];

  return (
    <div>
      <label htmlFor="country">Country:</label>
      <select
        id="country"
        value={country}
        onChange={(e) => setCountry(e.target.value)}
      >
        <option value="">-- Select a country --</option>
        {countries.map(c => (
          <option key={c.code} value={c.code}>
            {c.name}
          </option>
        ))}
      </select>
      {country && <p>Selected: {countries.find(c => c.code === country)?.name}</p>}
    </div>
  );
}

Multiple Select

function MultiSelect() {
  const [selected, setSelected] = useState([]);

  function handleChange(event) {
    const options = Array.from(event.target.selectedOptions);
    setSelected(options.map(o => o.value));
  }

  return (
    <div>
      <select multiple value={selected} onChange={handleChange} size={5}>
        <option value="react">React</option>
        <option value="vue">Vue</option>
        <option value="angular">Angular</option>
        <option value="svelte">Svelte</option>
        <option value="solid">Solid</option>
      </select>
      <p>Selected: {selected.join(', ') || 'none'}</p>
    </div>
  );
}

7. Controlled Checkbox

Single Checkbox (Boolean)

function AgreeCheckbox() {
  const [agreed, setAgreed] = useState(false);

  return (
    <div>
      <label>
        <input
          type="checkbox"
          checked={agreed}  // Note: "checked", not "value"
          onChange={(e) => setAgreed(e.target.checked)}
        />
        {' '}I agree to the terms and conditions
      </label>
      <button disabled={!agreed}>Continue</button>
    </div>
  );
}

Multiple Checkboxes (Array of Selected Values)

function InterestsCheckboxes() {
  const [interests, setInterests] = useState([]);

  const allInterests = ['React', 'Vue', 'Angular', 'Svelte', 'Next.js'];

  function handleChange(event) {
    const { value, checked } = event.target;

    setInterests(prev =>
      checked
        ? [...prev, value]          // Add
        : prev.filter(i => i !== value) // Remove
    );
  }

  return (
    <fieldset>
      <legend>Select your interests:</legend>
      {allInterests.map(interest => (
        <label key={interest} style={{ display: 'block' }}>
          <input
            type="checkbox"
            value={interest}
            checked={interests.includes(interest)}
            onChange={handleChange}
          />
          {' '}{interest}
        </label>
      ))}
      <p>Selected: {interests.join(', ') || 'none'}</p>
    </fieldset>
  );
}

8. Controlled Radio Buttons

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

  const plans = [
    { id: 'free', name: 'Free', price: '$0/mo', features: 'Basic features' },
    { id: 'pro', name: 'Pro', price: '$9/mo', features: 'All features' },
    { id: 'enterprise', name: 'Enterprise', price: '$29/mo', features: 'Everything + support' },
  ];

  return (
    <fieldset>
      <legend>Choose a plan:</legend>
      {plans.map(p => (
        <label
          key={p.id}
          style={{
            display: 'block',
            padding: 12,
            margin: 4,
            border: `2px solid ${plan === p.id ? '#0070f3' : '#eee'}`,
            borderRadius: 8,
            cursor: 'pointer',
          }}
        >
          <input
            type="radio"
            name="plan"
            value={p.id}
            checked={plan === p.id}
            onChange={(e) => setPlan(e.target.value)}
          />
          {' '}<strong>{p.name}</strong> — {p.price} ({p.features})
        </label>
      ))}
      <p>Current plan: <strong>{plans.find(p => p.id === plan)?.name}</strong></p>
    </fieldset>
  );
}

9. Controlled Number Input

function QuantityPicker({ min = 1, max = 99 }) {
  const [quantity, setQuantity] = useState(1);

  function handleChange(event) {
    const raw = event.target.value;
    
    // Allow empty string while typing
    if (raw === '') {
      setQuantity('');
      return;
    }

    const num = parseInt(raw, 10);
    if (!isNaN(num) && num >= min && num <= max) {
      setQuantity(num);
    }
  }

  function handleBlur() {
    // Reset to min if empty on blur
    if (quantity === '' || quantity < min) setQuantity(min);
  }

  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
      <button
        onClick={() => setQuantity(q => Math.max(min, q - 1))}
        disabled={quantity <= min}
      >
        −
      </button>
      <input
        type="number"
        value={quantity}
        onChange={handleChange}
        onBlur={handleBlur}
        min={min}
        max={max}
        style={{ width: 60, textAlign: 'center' }}
      />
      <button
        onClick={() => setQuantity(q => Math.min(max, q + 1))}
        disabled={quantity >= max}
      >
        +
      </button>
    </div>
  );
}

10. Controlled Range (Slider)

function BrightnessSlider() {
  const [brightness, setBrightness] = useState(50);

  return (
    <div>
      <label htmlFor="brightness">
        Brightness: {brightness}%
      </label>
      <input
        id="brightness"
        type="range"
        min={0}
        max={100}
        value={brightness}
        onChange={(e) => setBrightness(Number(e.target.value))}
      />
      <div
        style={{
          width: 100,
          height: 100,
          background: `hsl(0, 0%, ${brightness}%)`,
          border: '1px solid #ccc',
        }}
      />
    </div>
  );
}

Dual Range Slider

function PriceRange() {
  const [min, setMin] = useState(20);
  const [max, setMax] = useState(80);

  return (
    <div>
      <p>Price: ${min} — ${max}</p>
      <div>
        <label>Min: ${min}</label>
        <input
          type="range"
          min={0}
          max={100}
          value={min}
          onChange={(e) => {
            const val = Number(e.target.value);
            if (val < max) setMin(val);
          }}
        />
      </div>
      <div>
        <label>Max: ${max}</label>
        <input
          type="range"
          min={0}
          max={100}
          value={max}
          onChange={(e) => {
            const val = Number(e.target.value);
            if (val > min) setMax(val);
          }}
        />
      </div>
    </div>
  );
}

11. Controlled File Input — The Exception

File inputs cannot be controlled. Their value is read-only for security. You must use uncontrolled (ref) or just read on change.

function FileUpload() {
  const [file, setFile] = useState(null);
  const [preview, setPreview] = useState(null);

  function handleChange(event) {
    const selectedFile = event.target.files[0];
    if (!selectedFile) return;

    setFile(selectedFile);

    // Create preview for images
    if (selectedFile.type.startsWith('image/')) {
      const reader = new FileReader();
      reader.onload = (e) => setPreview(e.target.result);
      reader.readAsDataURL(selectedFile);
    }
  }

  function handleClear() {
    setFile(null);
    setPreview(null);
    // Can't reset input value — use a key to remount it
  }

  return (
    <div>
      {/* key={file?.name} forces remount to clear the input */}
      <input
        key={file?.name || 'empty'}
        type="file"
        accept="image/*"
        onChange={handleChange}
      />
      {file && (
        <div>
          <p>{file.name} ({(file.size / 1024).toFixed(1)} KB)</p>
          {preview && <img src={preview} alt="Preview" style={{ maxWidth: 200 }} />}
          <button onClick={handleClear}>Clear</button>
        </div>
      )}
    </div>
  );
}

12. Multi-Field Forms with a Single Handler

Instead of one handler per field, use the input's name attribute:

function RegistrationForm() {
  const [form, setForm] = useState({
    firstName: '',
    lastName: '',
    email: '',
    password: '',
    role: 'developer',
    newsletter: false,
  });

  // ONE handler for all fields
  function handleChange(event) {
    const { name, value, type, checked } = event.target;

    setForm(prev => ({
      ...prev,
      [name]: type === 'checkbox' ? checked : value,
    }));
  }

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

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="firstName"
        value={form.firstName}
        onChange={handleChange}
        placeholder="First name"
      />
      <input
        name="lastName"
        value={form.lastName}
        onChange={handleChange}
        placeholder="Last name"
      />
      <input
        name="email"
        type="email"
        value={form.email}
        onChange={handleChange}
        placeholder="Email"
      />
      <input
        name="password"
        type="password"
        value={form.password}
        onChange={handleChange}
        placeholder="Password"
      />
      <select name="role" value={form.role} onChange={handleChange}>
        <option value="developer">Developer</option>
        <option value="designer">Designer</option>
        <option value="manager">Manager</option>
      </select>
      <label>
        <input
          name="newsletter"
          type="checkbox"
          checked={form.newsletter}
          onChange={handleChange}
        />
        Subscribe to newsletter
      </label>
      <button type="submit">Register</button>
    </form>
  );
}

Why This Pattern Works

// The magic is computed property names:
const name = 'email';
const value = 'alice@example.com';

// This:
{ [name]: value }
// Becomes:
{ email: 'alice@example.com' }

// Combined with spread:
{ ...prev, [name]: value }
// Becomes:
{ firstName: 'Alice', lastName: 'Smith', email: 'alice@example.com', ... }

13. Input Formatting and Masking

Phone Number Formatting

function PhoneInput() {
  const [phone, setPhone] = useState('');

  function formatPhone(value) {
    // Remove all non-digits
    const digits = value.replace(/\D/g, '');

    // Format: (XXX) XXX-XXXX
    if (digits.length <= 3) return digits;
    if (digits.length <= 6) return `(${digits.slice(0, 3)}) ${digits.slice(3)}`;
    return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6, 10)}`;
  }

  function handleChange(event) {
    setPhone(formatPhone(event.target.value));
  }

  return (
    <input
      type="tel"
      value={phone}
      onChange={handleChange}
      placeholder="(555) 123-4567"
    />
  );
}

Credit Card Number Formatting

function CreditCardInput() {
  const [card, setCard] = useState('');

  function formatCard(value) {
    const digits = value.replace(/\D/g, '').slice(0, 16);
    // Add space every 4 digits: "1234 5678 9012 3456"
    return digits.replace(/(.{4})/g, '$1 ').trim();
  }

  function handleChange(event) {
    setCard(formatCard(event.target.value));
  }

  return (
    <input
      type="text"
      value={card}
      onChange={handleChange}
      placeholder="1234 5678 9012 3456"
      maxLength={19}
    />
  );
}

Currency Input

function CurrencyInput() {
  const [amount, setAmount] = useState('');

  function handleChange(event) {
    let value = event.target.value;

    // Allow only digits and one decimal point
    value = value.replace(/[^0-9.]/g, '');

    // Prevent multiple decimal points
    const parts = value.split('.');
    if (parts.length > 2) {
      value = parts[0] + '.' + parts.slice(1).join('');
    }

    // Limit to 2 decimal places
    if (parts[1] && parts[1].length > 2) {
      value = parts[0] + '.' + parts[1].slice(0, 2);
    }

    setAmount(value);
  }

  return (
    <div style={{ position: 'relative' }}>
      <span style={{ position: 'absolute', left: 12, top: 8 }}>$</span>
      <input
        type="text"
        value={amount}
        onChange={handleChange}
        placeholder="0.00"
        style={{ paddingLeft: 24 }}
      />
    </div>
  );
}

14. Real-Time Validation

Field-Level Validation

function ValidatedInput() {
  const [email, setEmail] = useState('');
  const [touched, setTouched] = useState(false);

  const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  const showError = touched && !isValid && email.length > 0;

  return (
    <div>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        onBlur={() => setTouched(true)}
        style={{
          border: `2px solid ${showError ? 'red' : touched && isValid ? 'green' : '#ccc'}`,
          borderRadius: 4,
          padding: '8px 12px',
        }}
        placeholder="your@email.com"
      />
      {showError && (
        <p style={{ color: 'red', fontSize: 12, margin: '4px 0 0' }}>
          Please enter a valid email address
        </p>
      )}
      {touched && isValid && (
        <p style={{ color: 'green', fontSize: 12, margin: '4px 0 0' }}>
          ✓ Valid email
        </p>
      )}
    </div>
  );
}

Password Strength Meter

function PasswordInput() {
  const [password, setPassword] = useState('');

  function getStrength(pw) {
    let score = 0;
    if (pw.length >= 8) score++;
    if (pw.length >= 12) score++;
    if (/[A-Z]/.test(pw)) score++;
    if (/[a-z]/.test(pw)) score++;
    if (/[0-9]/.test(pw)) score++;
    if (/[^A-Za-z0-9]/.test(pw)) score++;
    return score;
  }

  const strength = getStrength(password);
  const labels = ['', 'Very Weak', 'Weak', 'Fair', 'Good', 'Strong', 'Very Strong'];
  const colors = ['#ccc', '#ff4444', '#ff8800', '#ffcc00', '#88cc00', '#00cc44', '#00aa44'];

  return (
    <div>
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Enter password"
      />
      {password && (
        <div>
          <div style={{
            height: 4,
            borderRadius: 2,
            background: '#eee',
            marginTop: 8,
          }}>
            <div style={{
              height: '100%',
              width: `${(strength / 6) * 100}%`,
              background: colors[strength],
              borderRadius: 2,
              transition: 'all 0.3s',
            }} />
          </div>
          <span style={{ fontSize: 12, color: colors[strength] }}>
            {labels[strength]}
          </span>
          <ul style={{ fontSize: 12, margin: '4px 0' }}>
            <li style={{ color: password.length >= 8 ? 'green' : '#999' }}>
              {password.length >= 8 ? '✓' : '○'} At least 8 characters
            </li>
            <li style={{ color: /[A-Z]/.test(password) ? 'green' : '#999' }}>
              {/[A-Z]/.test(password) ? '✓' : '○'} Uppercase letter
            </li>
            <li style={{ color: /[a-z]/.test(password) ? 'green' : '#999' }}>
              {/[a-z]/.test(password) ? '✓' : '○'} Lowercase letter
            </li>
            <li style={{ color: /[0-9]/.test(password) ? 'green' : '#999' }}>
              {/[0-9]/.test(password) ? '✓' : '○'} Number
            </li>
            <li style={{ color: /[^A-Za-z0-9]/.test(password) ? 'green' : '#999' }}>
              {/[^A-Za-z0-9]/.test(password) ? '✓' : '○'} Special character
            </li>
          </ul>
        </div>
      )}
    </div>
  );
}

15. Common Mistakes and Anti-Patterns

Mistake 1: Missing onChange → Read-Only Input

// ❌ WARNING: This renders a read-only input
// React will warn: "You provided a `value` prop without an `onChange` handler"
<input value={name} />

// ✅ Add onChange handler
<input value={name} onChange={(e) => setName(e.target.value)} />

// ✅ Or use defaultValue for uncontrolled
<input defaultValue={name} />

// ✅ Or explicitly mark as readOnly
<input value={name} readOnly />

Mistake 2: Mixing value and defaultValue

// ❌ WRONG — don't use both
<input value={name} defaultValue="initial" />

// ✅ Controlled — use value + onChange
<input value={name} onChange={handleChange} />

// ✅ Uncontrolled — use defaultValue + ref
<input defaultValue="initial" ref={inputRef} />

Mistake 3: Setting State to undefined/null

// ❌ WRONG — switching from controlled to uncontrolled
const [name, setName] = useState('Alice');
// Later: setName(undefined) → React warns about switching modes

// ✅ CORRECT — always use string
const [name, setName] = useState('');
// To "clear": setName('') — always a string

Mistake 4: Not Using name Attribute

// ❌ TEDIOUS — one handler per field
function handleFirstNameChange(e) { setFirstName(e.target.value); }
function handleLastNameChange(e) { setLastName(e.target.value); }
function handleEmailChange(e) { setEmail(e.target.value); }

// ✅ BETTER — one handler, use name
function handleChange(e) {
  const { name, value } = e.target;
  setForm(prev => ({ ...prev, [name]: value }));
}

Mistake 5: Checkbox value vs checked

// ❌ WRONG — using value for checkbox state
<input type="checkbox" value={isChecked} onChange={handleChange} />

// ✅ CORRECT — use checked for checkbox
<input type="checkbox" checked={isChecked} onChange={(e) => setIsChecked(e.target.checked)} />

16. When to Use Uncontrolled Instead

Sometimes controlled inputs are overkill:

Use CaseRecommendedWhy
File uploadUncontrolledvalue is read-only on file inputs
Very large forms (50+ fields)Uncontrolled + FormDataAvoids massive re-render on each keystroke
Third-party DOM componentsUncontrolled + refThe library manages its own state
Simple fire-and-forgetUncontrolledNo need to track intermediate state
Real-time validation neededControlledYou need access to current value
Formatting as user typesControlledMust intercept and transform
Conditional field logicControlledNeed state to drive visibility

The FormData Alternative

function UncontrolledBigForm() {
  function handleSubmit(event) {
    event.preventDefault();
    const formData = new FormData(event.target);
    const data = Object.fromEntries(formData);
    console.log(data);
    // { firstName: "Alice", lastName: "Smith", email: "alice@x.com" }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="firstName" placeholder="First name" />
      <input name="lastName" placeholder="Last name" />
      <input name="email" type="email" placeholder="Email" />
      <button type="submit">Submit</button>
    </form>
  );
}

17. Key Takeaways

  1. Controlled inputs use value + onChange — React is the single source of truth
  2. Uncontrolled inputs use defaultValue + ref — DOM is the source of truth
  3. React's onChange fires on every keystroke, unlike HTML's native change event
  4. Use checked (not value) for checkboxes and radio buttons
  5. File inputs are always uncontrolled — their value is read-only
  6. Use the name attribute + computed property names for multi-field forms with one handler
  7. Formatting (phone, credit card, currency) is trivial with controlled inputs
  8. Real-time validation works best with controlled inputs + onBlur for "touched" state
  9. Never set controlled input value to undefined or null — always use a string
  10. Use defaultValue only for uncontrolled inputs — never mix with value

Explain-It Challenge

  1. The Restaurant Analogy: Explain controlled vs uncontrolled inputs using the analogy of a restaurant order. In one scenario, the waiter writes down exactly what you say (controlled). In another, you write your own order on a slip (uncontrolled). How does this relate to who "owns" the data?

  2. The Debugging Session: A junior developer writes <input value={name} /> and the input freezes — they can type but nothing appears. Walk through exactly what's happening in React's render cycle and how to fix it.

  3. The Technical Trade-off: You're building a form with 100 fields. Someone suggests making them all controlled. Explain the performance implications and when uncontrolled + FormData might be the better architectural choice.


Navigation: ← Handling Click Events · Next → Conditional Rendering Patterns