Controlled vs Uncontrolled Components in React

Controlled vs Uncontrolled Components in React

Understanding controlled vs uncontrolled in React is about one question: who owns the state? Learn both meanings for form inputs and component design patterns.

Aurora Scharff

Aurora Scharff

December 8, 2025

The terms "controlled" and "uncontrolled" appear frequently in React documentation, but they can be confusing because they seem to mean different things in different contexts. In reality, they're asking the same question: who owns the state—the parent or the component itself?

This article clarifies the terminology and helps you recognize the pattern in everyday React code.

The Core Question: Who Owns the State?

Both uses of "controlled" and "uncontrolled" share the same underlying concept:

  • Controlled: State is owned externally and passed in via props
  • Uncontrolled: State is owned internally by the component itself

For form inputs, "internal state" means the DOM manages the value. For custom components, "internal state" typically means React's useState. But it's the same question: does the parent control the state, or does the component manage it internally?

This distinction matters because it determines where the "source of truth" lives and who can change it.

Form Inputs: The Common Introduction

The terms are commonly introduced with form inputs, where the question is whether the parent component controls the input's value via props, or the input manages its own value internally via the DOM.

A controlled input has its value managed by the parent through React state:

      function SearchForm() {
  const [query, setQuery] = useState('');

  function handleSubmit(event) {
    event.preventDefault();
    console.log(query);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      <button type="submit">Search</button>
    </form>
  );
}

    

React fully controls this input's value. The value prop sets what's displayed, and onChange updates the state. Anything that updates query—whether typing, a "Clear" button, or selecting a suggestion—will update the input.

This means value and onChange must work together. If you pass value without onChange, it will be impossible to type into the input—React will revert every keystroke back to the value you specified.

An uncontrolled input manages its value internally via the DOM:

      function SearchForm() {
  function handleSubmit(event) {
    event.preventDefault();
    const formData = new FormData(event.target);
    console.log(formData.get('query'));
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="query" defaultValue="" />
      <button type="submit">Search</button>
    </form>
  );
}

    

React doesn't know about this input's value—it doesn't set or update it, and doesn't know when it changes. The defaultValue prop only sets the initial value; after that, the DOM owns the state.

Components: Props vs Internal State

Beyond forms, "controlled" and "uncontrolled" describe a general component design pattern. The same question applies: does the parent own the state via props, or does the component manage it internally?

Consider a simple Toggle component:

      function Toggle({ label }) {
  const [isOn, setIsOn] = useState(false);

  return (
    <button onClick={() => setIsOn(!isOn)}>
      {label}: {isOn ? 'ON' : 'OFF'}
    </button>
  );
}

    

This component uses useState, so React is managing the state. But that doesn't make it "controlled" in the component design sense. The parent component has no way to read or set the toggle's value:

      function App() {
  return <Toggle label="Dark Mode" />;
}

    

Since the parent can't influence the state, this Toggle is uncontrolled—it manages its own internal state, and the parent just renders it.

Making Components Controlled

To make Toggle controlled, we move the state to the parent and pass it via props:

      function Toggle({ label, isOn, onToggle }) {
  return (
    <button onClick={onToggle}>
      {label}: {isOn ? 'ON' : 'OFF'}
    </button>
  );
}

function App() {
  const [darkMode, setDarkMode] = useState(false);

  return (
    <>
      <Toggle
        label="Dark Mode"
        isOn={darkMode}
        onToggle={() => setDarkMode(!darkMode)}
      />
      <p>Theme: {darkMode ? 'dark' : 'light'}</p>
    </>
  );
}

    

Now App controls the toggle's value. It can read the state to update the theme, reset it, or coordinate multiple toggles. The component renders from props instead of internal state.

Notice how this mirrors native inputs: controlled inputs use value and onChange, and our controlled Toggle uses a similar pattern with isOn and onToggle. Modeling custom components after native HTML elements makes their APIs intuitive and predictable.

Supporting Both Modes

Some components support both controlled and uncontrolled usage, just like native <input> elements. For our Toggle, it could look like this:

      function Toggle({ label, isOn, onToggle, defaultOn = false }) {
  const [internalOn, setInternalOn] = useState(defaultOn);

  const isControlled = isOn !== undefined;
  const on = isControlled ? isOn : internalOn;

  function handleToggle() {
    if (isControlled) {
      onToggle?.();
    } else {
      setInternalOn(!internalOn);
    }
  }

  return (
    <button onClick={handleToggle}>
      {label}: {on ? 'ON' : 'OFF'}
    </button>
  );
}

    

This allows flexible usage:

      // Uncontrolled - Toggle manages its own state
<Toggle label="Notifications" defaultOn={true} />

// Controlled - Parent manages the state
<Toggle label="Dark Mode" isOn={darkMode} onToggle={() => setDarkMode(!darkMode)} />

    

The key is checking isOn !== undefined to determine the mode. When controlled, the component uses the prop and delegates changes to the parent via onToggle. When uncontrolled, it manages its own state internally.

One important rule: a component shouldn't switch between controlled and uncontrolled modes during its lifetime. If isOn starts as undefined and later becomes a value (or vice versa), the behavior becomes unpredictable. You may have seen the warning "A component is changing an uncontrolled input to be controlled"—this is React telling you that an input switched modes, and custom components should follow the same principle.

Many UI libraries use this pattern. However, supporting both modes adds complexity. For simpler components, it's often better to choose one approach.

Real-World Examples

The controlled/uncontrolled pattern appears throughout React UI libraries:

Tabs: An uncontrolled tabs component manages which tab is active internally. A controlled version accepts activeTab and onTabChange props, letting the parent programmatically change tabs or sync with URL state.

Modals: An uncontrolled modal might have an internal isOpen state and expose a trigger element. A controlled modal accepts isOpen and onClose props, letting the parent control visibility based on application state.

Accordions: An uncontrolled accordion lets each panel manage its own open/closed state. A controlled version accepts expandedPanels and onChange props, enabling "single panel open" behavior that requires coordination.

Date Pickers: An uncontrolled date picker manages selection internally. A controlled version accepts selectedDate and onChange, letting the parent validate dates, sync with other fields, or implement date range constraints.

The key insight is that controlled components enable coordination. When multiple components need to stay in sync, or when parents need to implement complex behavior, controlled components make this possible.

Trade-offs and Considerations

The choice between controlled and uncontrolled comes down to one question: does the parent need to know about or influence the state?

Form Inputs

For forms, uncontrolled inputs are often simpler. You let the DOM handle the state and read values on submit using FormData. This approach works well with React's modern form features like the <form> action prop, useFormStatus, and useActionState.

Controlled inputs become valuable when you need the value while the user is typing—for real-time validation, transforming input (like formatting phone numbers), or coordinating multiple fields. The trade-off is that controlled inputs re-render on every keystroke, while uncontrolled inputs avoid this.

Form libraries like React Hook Form navigate this trade-off by using uncontrolled inputs with refs by default for performance, while still providing validation and error handling. They support controlled inputs via a Controller component when the parent needs access to intermediate values.

Custom Components

For custom components, the question is whether the component should work independently or be coordinated by its parent.

An uncontrolled component manages its own state and works out of the box—the parent just renders it. This is simpler when the state is an implementation detail the parent doesn't care about.

A controlled component receives its state via props and notifies the parent of changes. This enables coordination: the parent can sync multiple instances, implement complex behavior like "only one open at a time," or integrate the state with the rest of the application. The trade-off is more wiring in the parent component.

When building reusable component libraries, supporting both modes gives consumers flexibility—but for application code, picking one approach keeps things simpler.

Conclusion

The terms "controlled" and "uncontrolled" always ask the same question: who owns the state? Whether it's a form input (where internal state lives in the DOM) or a custom component (where internal state lives in useState), the choice is between props from the parent and internal state managed by the component.

The key insight is that using useState doesn't make a component "controlled." From the parent's view, a component is uncontrolled if the parent can't influence its state. Understanding this helps you design component APIs that give parents the right level of control for their needs.


Sources:

More certificates.dev articles

Get the latest news and updates on developer certifications. Content is updated regularly, so please make sure to bookmark this page or sign up to get the latest content directly in your inbox.

Looking for Certified Developers?

We can help you recruit Certified Developers for your organization or project. The team has helped many customers employ suitable resources from a pool of 100s of qualified Developers.

Let us help you get the resources you need.

Contact Us
Customer Testimonial for Hiring
like a breath of fresh air
Everett Owyoung
Everett Owyoung
Head of Talent for ThousandEyes
(a Cisco company)