Skip to main content

πŸŽ›οΈ Controlled vs Uncontrolled Components

React gives you two ways to manage a form input: let React state be the boss (controlled), or let the DOM keep the value and read it only when you need it (uncontrolled). Knowing the trade-offs β€” and when to blend the two β€” is what separates tidy forms from fiddly ones.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Define controlled and uncontrolled components and identify each in code
  • Contrast their source of truth, data access, and re-render behavior
  • Choose the right approach for a given form using a repeatable decision flow
  • Handle file inputs, which are always uncontrolled
  • Build hybrid components that combine state with a ref

Estimated Time: 30–40 minutes  β€’  Difficulty: Intermediate

Hands-on: Convert a controlled form to uncontrolled, then build a hybrid search box.

In This Lesson

Two Ways to Hold a Value

Every form input has a value that lives somewhere. The only question is who owns it: React state, or the DOM node itself. That single choice defines the two patterns.

πŸ’‘ Analogy: A controlled component is like dictating to an assistant who writes down every word as you speak β€” you always know exactly what's on the page. An uncontrolled component is handing someone a notepad and only reading it when they hand it back β€” simpler, but you don't see it change in real time.
graph TD A[Form input in React] A --> B[Controlled] A --> C[Uncontrolled] B --> D[React state owns the value] B --> E[onChange updates state, component re-renders] C --> F[DOM owns the value] C --> G[Read it via a ref when needed]

Controlled Components

A controlled component takes its current value from state and reports changes back through onChange. React is the single source of truth, so the input can never drift out of sync with your data.

import { useState } from 'react';

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

  return (
    <div>
      <input
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder="Type something…"
      />
      <p>You typed: {value}</p>  {/* always up to date */}
    </div>
  );
}

Strengths

  • Live value β€” the current text is always in state, ready for validation or display.
  • Predictable β€” one source of truth means fewer surprises.
  • Reactive UI β€” enable/disable buttons, show hints, or reformat as the user types.
  • Coordination β€” one field can easily react to another.

Costs

  • More code: state + handler per field (or the single-object pattern).
  • A re-render on every keystroke β€” rarely a problem, occasionally worth optimizing.

βœ… The React team's default

Controlled components are the recommended starting point. Most forms you build should be controlled; reach for uncontrolled only when there's a concrete reason to.

Uncontrolled Components

An uncontrolled component lets the DOM keep the value. You set an initial value with defaultValue (not value) and grab the current value with a ref when you need it β€” usually at submit time.

import { useRef } from 'react';

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

  const handleSubmit = (e) => {
    e.preventDefault();
    alert(`You entered: ${inputRef.current.value}`); // read on demand
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={inputRef} defaultValue="" placeholder="Type something…" />
      <button type="submit">Submit</button>
    </form>
  );
}

πŸ“– value vs. defaultValue

value makes React own the input (controlled) β€” it must be paired with onChange.

defaultValue sets the input's initial value once and then steps back, letting the DOM take over (uncontrolled).

Strengths

  • Less code for simple forms β€” no state, no change handler.
  • No per-keystroke re-render.
  • Easy interop with non-React code and third-party DOM libraries.

Costs

  • No live value β€” you only see it when you ask via the ref.
  • Harder to do real-time validation or coordinate fields.

Side by Side

AspectControlledUncontrolled
Source of truthReact stateThe DOM
Set initial valuevalue propdefaultValue prop
Read the valueAnytime, from stateOn demand, via a ref
Change handleronChange requiredOptional
Re-rendersOn every changeOnly when you trigger one
ValidationReal-time, easyUsually on submit
Code volumeMoreLess

The same two-field form, both ways

// CONTROLLED β€” state is the source of truth
function ControlledForm() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log({ name, email });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <button type="submit">Submit</button>
    </form>
  );
}
// UNCONTROLLED β€” the DOM holds the values; refs read them at submit
function UncontrolledForm() {
  const nameRef = useRef(null);
  const emailRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log({ name: nameRef.current.value, email: emailRef.current.value });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={nameRef} defaultValue="" />
      <input ref={emailRef} defaultValue="" />
      <button type="submit">Submit</button>
    </form>
  );
}

Choosing an Approach

When you're unsure, walk this quick decision tree. In practice most branches lead to "controlled" β€” which is exactly why it's the default.

flowchart TD A{Need the value
as the user types?} -->|Yes| B[Controlled] A -->|No| C{File input?} C -->|Yes| D[Uncontrolled] C -->|No| E{Integrating a
non-React widget?} E -->|Yes| D E -->|No| F{Simple form, validate
only on submit?} F -->|Yes| D F -->|No| B

πŸ’‘ Rules of thumb

Controlled when you need live validation, format-as-you-type, dependent fields, a disabled-until-valid button, or auto-save.

Uncontrolled for file inputs, quick throwaway forms, or when bridging React with a library that manipulates the DOM directly.

Special Cases & Hybrids

File inputs are always uncontrolled

For security, a browser won't let JavaScript set a file input's value, so <input type="file"> can't be controlled. Read the chosen files from the event or a ref β€” and you can still keep a little controlled state alongside to show feedback:

import { useRef, useState } from 'react';

function FileUploader() {
  const fileRef = useRef(null);
  const [fileName, setFileName] = useState('');

  const handleChange = (e) => {
    const file = e.target.files[0];
    if (file) setFileName(file.name); // controlled state for display
  };

  return (
    <div>
      <input type="file" ref={fileRef} onChange={handleChange} />
      {fileName && <p>Selected: {fileName}</p>}
    </div>
  );
}

Controlled input + ref for DOM actions

A controlled input can still hold a ref when you need a genuine DOM operation like focusing. Here the value is controlled, but the ref lets us refocus after clearing:

function SearchBox() {
  const [query, setQuery] = useState('');
  const inputRef = useRef(null);

  const handleClear = () => {
    setQuery('');
    inputRef.current.focus(); // DOM action the state can't express
  };

  return (
    <div>
      <input
        ref={inputRef}
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search…"
      />
      {query && <button type="button" onClick={handleClear}>Clear</button>}
    </div>
  );
}

Seeding controlled state from an API

When editing existing data, fetch it, drop it into state, and go fully controlled from there:

function ProfileEditor({ userId }) {
  const [profile, setProfile] = useState(null);

  useEffect(() => {
    let active = true;
    fetch(`/api/users/${userId}`)
      .then((r) => r.json())
      .then((data) => { if (active) setProfile(data); });
    return () => { active = false; }; // avoid setting state after unmount
  }, [userId]);

  if (!profile) return <p>Loading…</p>;

  const handleChange = (e) => {
    const { name, value } = e.target;
    setProfile((prev) => ({ ...prev, [name]: value }));
  };

  return (
    <input name="displayName" value={profile.displayName} onChange={handleChange} />
  );
}

βœ… Hybrids are normal

Mixing controlled state with a ref isn't a hack β€” it's the idiomatic way to get React's predictability and occasional direct DOM access. Use state for the value, a ref for the imperative bits.

Hands-on Exercise

πŸ‹οΈ Convert, Then Combine

Objective: Prove you can move between the two patterns and blend them.

Part A β€” convert to uncontrolled:

Take this controlled contact form and rewrite it with refs and defaultValue instead of state.

function ControlledContact() {
  const [form, setForm] = useState({ name: '', email: '', message: '' });
  const handleChange = (e) =>
    setForm((p) => ({ ...p, [e.target.name]: e.target.value }));
  const handleSubmit = (e) => { e.preventDefault(); console.log(form); };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" value={form.name} onChange={handleChange} />
      <input name="email" value={form.email} onChange={handleChange} />
      <textarea name="message" value={form.message} onChange={handleChange} />
      <button type="submit">Send</button>
    </form>
  );
}

Part B β€” build a hybrid:

Create a search box whose text is controlled, plus a "Clear" button that empties it and re-focuses the field using a ref.

πŸ’‘ Hint

For Part A, create one ref per field, drop the state and onChange, swap value for defaultValue, and read ref.current.value inside handleSubmit. For Part B, keep useState for the text and add a useRef only for the focus call.

βœ… Sample solution
// Part A β€” uncontrolled
import { useRef } from 'react';

function UncontrolledContact() {
  const nameRef = useRef(null);
  const emailRef = useRef(null);
  const messageRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log({
      name: nameRef.current.value,
      email: emailRef.current.value,
      message: messageRef.current.value,
    });
    e.target.reset();
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" ref={nameRef} defaultValue="" />
      <input name="email" ref={emailRef} defaultValue="" />
      <textarea name="message" ref={messageRef} defaultValue="" />
      <button type="submit">Send</button>
    </form>
  );
}

// Part B β€” hybrid search box
import { useState, useRef } from 'react';

function SearchBox() {
  const [query, setQuery] = useState('');
  const inputRef = useRef(null);

  const clear = () => {
    setQuery('');
    inputRef.current.focus();
  };

  return (
    <div>
      <input
        ref={inputRef}
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search…"
      />
      {query && <button type="button" onClick={clear}>Clear</button>}
    </div>
  );
}

🎯 Quick Quiz

Question 1: In an uncontrolled component, where does the input's value live?

Question 2: Why can't a <input type="file"> be controlled?

Question 3: Which prop sets an input's initial value without making it controlled?

Best Practices

βœ… Do❌ Avoid
Default to controlled componentsReaching for refs just to save a few lines
Use defaultValue for uncontrolled inputsMixing value and defaultValue on one input
Keep file inputs uncontrolledTrying to set a file input's value
Combine state + ref for DOM actions (focus, scroll)Duplicating the value in both state and a ref
Pick one pattern per input and stay consistentSwitching an input between controlled and uncontrolled mid-life

⚠️ Don't flip an input mid-stream

If value starts as undefined and later becomes a string, React logs: "A component is changing an uncontrolled input to be controlled." Decide up front and initialize state with a defined value to avoid it.

Summary & Quiz

πŸŽ‰ Key Takeaways

  • Controlled: React state owns the value; bind value + onChange. Best for real-time validation and reactive UIs β€” and the recommended default.
  • Uncontrolled: the DOM owns the value; set defaultValue and read it via a ref. Less code, no per-keystroke re-render.
  • value β‡’ controlled; defaultValue β‡’ uncontrolled. Never mix them on one input.
  • File inputs are always uncontrolled for security reasons.
  • Hybrids β€” controlled value plus a ref for focus/scroll β€” are idiomatic and common.

πŸ“š Further Reading

πŸš€ What's Next?

We touched useEffect when seeding state from an API. Next we go deep on it: the useEffect Hook for side effects β€” data fetching, subscriptions, timers, and cleaning them all up correctly.

πŸŽ‰ Decision made!

You can now pick controlled, uncontrolled, or a deliberate blend β€” and defend the choice.