Controlled vs Uncontrolled Components

Every form in the MERN Blog โ€” login, register, create post, edit post โ€” needs to collect input from the user and send it to the Express API. React offers two approaches for managing form inputs: controlled components, where React state owns the input value and every keystroke updates state, and uncontrolled components, where the DOM owns the value and you read it via a ref when needed. Understanding the difference โ€” and knowing which pattern to use โ€” is the foundation of every form you will build in the MERN stack.

Controlled vs Uncontrolled โ€” Side by Side

Controlled Component Uncontrolled Component
Who owns the value React state (useState) The DOM
How to read the value Read the state variable Read via a ref (useRef)
Updates via onChange โ†’ setState No handler required โ€” DOM updates itself
Validation Easy โ€” validate state on any change or submit Harder โ€” must read ref to validate
Programmatic set Easy โ€” call setState('') to clear Must set ref.current.value = ''
Recommended for MERN โœ… Yes โ€” standard pattern Only for simple one-off reads (file uploads, focus)
Note: A controlled input is one where you provide both a value prop and an onChange handler. React then owns the value โ€” the input displays whatever state holds, and every keystroke calls onChange to update state. An uncontrolled input has no value prop โ€” the browser manages the value independently and you read it via a ref when you need it (typically on form submit). React’s official guidance recommends controlled components for most forms.
Tip: If you set a value prop on an input without an onChange handler, React makes the input read-only and logs a warning. This is a common mistake when first learning controlled inputs. Always pair value with onChange. If you want a read-only input intentionally, use readOnly instead: <input value={savedValue} readOnly />.
Warning: Never mix controlled and uncontrolled modes for the same input. Starting an input as uncontrolled (value is undefined) and then providing a value prop later causes a React warning: “A component is changing an uncontrolled input to be controlled.” Always initialise controlled inputs with a defined value โ€” use an empty string for text inputs, not undefined or null: useState('') not useState(null).

Controlled Component โ€” Full Example

import { useState } from 'react';

function SearchInput({ onSearch }) {
  // React owns the value
  const [query, setQuery] = useState(''); // โœ“ initialised to empty string, not null

  const handleChange = (e) => {
    setQuery(e.target.value);  // every keystroke updates state
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    onSearch(query);           // read from state โ€” always current
  };

  const handleClear = () => {
    setQuery('');              // programmatic clear โ€” easy with controlled
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={query}           {/* controlled: value comes from state */}
        onChange={handleChange} {/* every keystroke syncs DOM โ†’ state */}
        placeholder="Search posts..."
      />
      {query && <button type="button" onClick={handleClear}>ร—</button>}
      <button type="submit">Search</button>
    </form>
  );
}

Uncontrolled Component โ€” When to Use

import { useRef } from 'react';

// Uncontrolled: useful when you only need the value on submit
// and do not need live validation or programmatic updates
function SimpleSearchForm({ onSearch }) {
  const inputRef = useRef(null); // ref points to the DOM input

  const handleSubmit = (e) => {
    e.preventDefault();
    onSearch(inputRef.current.value); // read from DOM on submit
    inputRef.current.value = '';      // clear by mutating the DOM ref
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        ref={inputRef}           {/* no value prop โ€” DOM owns value */}
        placeholder="Search..."
        defaultValue=""          {/* initial value (not controlled) */}
      />
      <button type="submit">Search</button>
    </form>
  );
}

// โ”€โ”€ When uncontrolled is appropriate โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// โœ“ File inputs: <input type="file" ref={fileRef} /> (file inputs cannot be controlled)
// โœ“ One-off reads with no validation or live feedback needed
// โœ“ Integrating with non-React DOM libraries that manage their own state

// โ”€โ”€ Controlled is better when โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// โœ“ Live validation (show errors as user types)
// โœ“ Conditional disabling based on input value
// โœ“ Transforming input (auto-capitalise, format phone number)
// โœ“ Programmatic reset after form submission
// โœ“ Pre-populating edit forms from fetched data

Generic onChange Handler for Multiple Fields

// One handler for all form fields using the input's name attribute
function LoginForm() {
  const [formData, setFormData] = useState({ email: '', password: '' });

  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    setFormData(prev => ({
      ...prev,
      // Computed property key: uses the input's name attribute
      [name]: type === 'checkbox' ? checked : value,
    }));
  };

  return (
    <form>
      <input
        name="email"    {/* name must match the state key */}
        type="email"
        value={formData.email}
        onChange={handleChange}
      />
      <input
        name="password"
        type="password"
        value={formData.password}
        onChange={handleChange}
      />
      <input
        name="rememberMe"
        type="checkbox"
        checked={formData.rememberMe}
        onChange={handleChange}
      />
    </form>
  );
}

Common Mistakes

Mistake 1 โ€” Setting value without onChange (accidental read-only)

โŒ Wrong โ€” controlled input with no way to update it:

<input value={email} />
{/* React warning: value prop without onChange โ€” input is read-only */}

โœ… Correct โ€” always pair value with onChange:

<input value={email} onChange={e => setEmail(e.target.value)} /> {/* โœ“ */}

Mistake 2 โ€” Initialising controlled state with null or undefined

โŒ Wrong โ€” undefined value makes React treat input as uncontrolled initially:

const [email, setEmail] = useState(null); // null โ†’ uncontrolled
// Warning: changing uncontrolled input to controlled

โœ… Correct โ€” initialise to empty string:

const [email, setEmail] = useState(''); {/* '' โ†’ controlled from the start โœ“ */}

Mistake 3 โ€” Forgetting to use the name attribute for the generic handler

โŒ Wrong โ€” name attribute missing means [undefined] key is set:

<input value={formData.email} onChange={handleChange} />
{/* e.target.name is '' โ†’ setFormData sets { '': value } not { email: value } */}

โœ… Correct โ€” always match the name to the state key:

<input name="email" value={formData.email} onChange={handleChange} /> {/* โœ“ */}

Quick Reference

Pattern Code
Controlled text input <input value={val} onChange={e => setVal(e.target.value)} />
Controlled checkbox <input type="checkbox" checked={bool} onChange={e => setFlag(e.target.checked)} />
Controlled select <select value={val} onChange={e => setVal(e.target.value)}>
Controlled textarea <textarea value={text} onChange={e => setText(e.target.value)} />
Generic multi-field handler setForm(prev => ({ ...prev, [e.target.name]: e.target.value }))
Uncontrolled ref const ref = useRef(); ref.current.value
Reset controlled form setFormData({ email: '', password: '' })

🧠 Test Yourself

You initialise form state as useState({ email: null, password: null }) and bind value={formData.email}. React logs a warning. What is the warning and how do you fix it?