Skip to content

React · State and Data Flow

Controlled vs Uncontrolled Components in React — A Complete Guide

7 min read Updated 2026-06-24 Share:

Practice Controlled vs Uncontrolled Components interview questions

The core question: who owns the value?

Every form element in React has one fundamental question: who is the source of truth for its current value — React state or the DOM?

  • Controlled component → React state is the source of truth. The value prop drives the DOM. Every keystroke goes through state.
  • Uncontrolled component → The DOM is the source of truth. React sets the initial value and steps back. You read the value only when you need it.

Everything else follows from this distinction.

Controlled components in depth

A controlled input is one where value is driven by React state and every change is funnelled through onChange:

function EmailField() {
  const [email, setEmail] = useState('')

  return (
    <input
      type="email"
      value={email}                           // state → DOM
      onChange={e => setEmail(e.target.value)} // DOM event → state
    />
  )
}

This creates a tight feedback loop. React knows the field's value at all times and can:

  • Validate on every keystroke and show/hide error messages
  • Disable a submit button until the form is complete
  • Transform input (uppercase, format phone numbers, trim whitespace)
  • Reset the form by simply calling setState(initialValues)
  • Read the complete form data in the submit handler without touching the DOM

The cost is that you must wire value + onChange for every field. For large forms this is repetitive — which is why libraries like React Hook Form exist.

The read-only trap

If you pass value without onChange, React freezes the input — users type but the DOM value is reset to value on every render. React warns:

"You provided a value prop to a form field without an onChange handler. This will render a read-only field."

Fix it with onChange, or use readOnly if the intent is truly read-only:

// ❌ Frozen — user can't type
<input value={someValue} />

// ✅ Controlled
<input value={someValue} onChange={e => setValue(e.target.value)} />

// ✅ Intentionally read-only
<input value={someValue} readOnly />

Starting controlled with an empty string

Never initialise controlled state with null or undefined. That starts the input as uncontrolled (React treats a null value as "no value"), and when you later assign a real string React will warn about switching from uncontrolled to controlled.

// ❌ Starts uncontrolled → triggers warning when set to a real string
const [text, setText] = useState(null)

// ✅ Always start with a string (even empty)
const [text, setText] = useState('')

Uncontrolled components in depth

An uncontrolled input lets the browser manage its own value. You attach a ref to read the value imperatively when needed:

function SearchBox() {
  const inputRef = useRef(null)

  function handleSubmit(e) {
    e.preventDefault()
    performSearch(inputRef.current.value)  // read on demand
  }

  return (
    <form onSubmit={handleSubmit}>
      <input ref={inputRef} defaultValue="" />
      <button type="submit">Search</button>
    </form>
  )
}

defaultValue sets the initial value without making the input controlled. After that, the DOM owns the value.

When uncontrolled is appropriate

  • Submit-only forms where you need the value once, not on every keystroke
  • File inputs<input type="file"> is always uncontrolled. The browser controls the file path for security; you can never set value on it
  • Integrating third-party DOM libraries that manipulate inputs directly (rich text editors, date pickers that aren't React-aware)
// File input — must use ref, value prop is not supported
function FileUpload() {
  const fileRef = useRef(null)

  function upload() {
    const file = fileRef.current.files[0]
    if (file) formData.append('file', file)
  }

  return <input type="file" ref={fileRef} />
}

value vs defaultValue — the complete picture

PropEffect
valueInput is controlled — React drives the DOM value on every render
defaultValueInput is uncontrolled — sets the initial DOM value, then DOM takes over
NeitherInput is uncontrolled with no initial value

The same pattern applies to <textarea> and <select>:

// Controlled textarea
<textarea value={text} onChange={e => setText(e.target.value)} />

// Controlled select — value matches an <option>'s value
<select value={chosen} onChange={e => setChosen(e.target.value)}>
  <option value="a">A</option>
  <option value="b">B</option>
</select>

// Uncontrolled select with default
<select defaultValue="b" ref={selectRef}>
  <option value="a">A</option>
  <option value="b">B</option>
</select>

Programmatic reset

Controlled forms reset with a single state update:

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

function handleReset() { setForm(EMPTY) }  // ← all inputs snap to empty

Uncontrolled forms require manually setting each ref:

function handleReset() {
  nameRef.current.value  = ''
  emailRef.current.value = ''
  // repeat for every field
}

Controlled wins for reset, prefill, and programmatic form manipulation.

Dynamic field lists

Controlled is especially powerful for dynamic forms:

function TagsInput() {
  const [tags, setTags] = useState(['react'])

  return (
    <>
      {tags.map((tag, i) => (
        <input
          key={i}
          value={tag}
          onChange={e => setTags(prev =>
            prev.map((t, idx) => idx === i ? e.target.value : t)
          )}
        />
      ))}
      <button onClick={() => setTags(t => [...t, ''])}>+ Add</button>
    </>
  )
}

With uncontrolled inputs you'd need to track refs in an array and manually read each one — significantly more code for the same behaviour.

How React Hook Form bridges the gap

React Hook Form uses uncontrolled inputs internally but gives you a controlled-like developer experience. It subscribes to DOM change events directly without storing every keystroke in React state, which means far fewer re-renders:

import { useForm } from 'react-hook-form'

function SignupForm() {
  const {
    register,
    handleSubmit,
    formState: { errors }
  } = useForm()

  return (
    <form onSubmit={handleSubmit(data => api.signup(data))}>
      <input
        {...register('email', {
          required: 'Email is required',
          pattern: { value: /@/, message: 'Invalid email' }
        })}
      />
      {errors.email && <p>{errors.email.message}</p>}
      <button type="submit">Sign up</button>
    </form>
  )
}

register wires up a ref and an onChange handler internally — the component re-renders only when validation state changes or on submit, not on every keystroke.

For controlled integration (when the input value must flow back to React state), use Controller or useController.

Quick decision guide

NeedPrefer
Live validationControlled
Submit-only, simple formUncontrolled
File uploadUncontrolled (forced)
Programmatic reset/prefillControlled
Large complex form with performance concernsReact Hook Form (uncontrolled internally)
Integration with non-React DOM libraryUncontrolled

Key interview points

  • Controlled: React state owns the value via value + onChange. Full power, some boilerplate.
  • Uncontrolled: DOM owns the value after mount. Read it with a ref. Simpler, less control.
  • value → controlled. defaultValue → uncontrolled seed.
  • Never pass null/undefined as the value prop — use '' instead.
  • <input type="file"> is always uncontrolled.
  • React Hook Form uses uncontrolled internals for performance but controlled ergonomics in the API.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel