Uncontrolled or controlled: A matter of perspective

Sam Selikoff

Sam Selikoff

You may have come across the terms uncontrolled component and controlled component in your React journey.

These terms are typically introduced by looking at the two ways native input elements can be used in React: as uncontrolled inputs, or as controlled inputs.

An uncontrolled input is an input whose state lives entirely in the DOM:

function Page() {
  return <input name="search" />;
}

React doesn't know about this input's value. It doesn't set or update it, and it doesn't know when it changes. In this sense, this input's value is uncontrolled by React.

A controlled input, on the other hand, delegates its state to React:

function Page() {
  const [search, setSearch] = useState('');

  return (
   <> 
    <input
      value={search}
      onChange={(e) => setSearch(e.target.value)}
      name="search"
    />
    
    <button onClick={() => setSearch('')}>Clear</button>
   </>
  );
}

React fully controls this input's value. The initial value of the search state, as well as any subsequent updates, will always be reflected by the input. Importantly, not only will typing in the input update its value (thanks to the onChange handler), but anything else that updates the search state – like the Clear button – will also update the input.

In this sense, this input's value is controlled by React.


But uncontrolled and controlled components don't stop with inputs.

Take this simple Counter — everyone's favorite React component:

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <>
      <button onClick={() => setCount(count - 1)}>-</button>
      <div>{count}</div>
      <button onClick={() => setCount(count + 1)}>+</button>
    </>
  );
}

Is it uncontrolled or controlled?

Well, React is definitely controlling the count state. So it seems like that would make Counter a controlled component.

But what about from the perspective of an App that renders it?

function App() {
  return <Counter />;
}

Looks a lot like the uncontrolled input above, doesn't it?


Let's make a change to Counter.

Instead of giving it internal state, let's have it render from props:

function App() {
  const [count, setCount] = useState(0);

  return (
    <>
      <Counter value={count} onChange={setCount} />

      <p>App state: {count}</p>

      <button onClick={() => setCount(0)}>Reset</button>
    </>
  );
}

Counter is now being controlled by App. Instead of managing its own internal state, it has delegated that state to its parent.

So, from the perspective of App, the first Counter was an uncontrolled component, and the second was a controlled component.

It seems like we've found a simple heuristic: if a component has internal state, it's uncontrolled, and if it doesn't, it's controlled.

...almost.

Let's add one more wrinkle.


What would it look like if we brought back our counter's internal state, but added the ability for it to optionally render from props?

Something like this:

function Counter({ value, onChange }) {
  const [internalCount, setInternalCount] = useState(0);
  const count = value ?? internalCount;

  function increment() {
    if (onChange) {
      onChange(value + 1);
    } else {
      setInternalCount(internalCount + 1);
    }
  }

  function decrement() {
    if (onChange) {
      onChange(value - 1);
    } else {
      setInternalCount(internalCount - 1);
    }
  }

  return (
    <>
      <button onClick={decrement}>-</button>
      <div>{count}</div>
      <button onClick={increment}>+</button>
    </>
  );
}

Here's the key line:

const count = value ?? internalCount;

If the value prop is not passed in, our counter falls back to its internal state as its source of truth for rendering.

Let's update App to render two instances of Counter: one that doesn't pass in props, and one that does:

function App() {
  const [count, setCount] = useState(0);

  return (
    <>
      <div>
        <Counter />
      </div>
      
      <div>
        <Counter value={count} onChange={setCount} />
        <span>App state: {count}</span>
      </div>
    </>
  );
}

From the perspective of App, the left counter is uncontrolled, because it manages its own internal state. But the right counter is controlled. App gets to determine what its value is, and how that value gets updated.


So — we've written a Counter that can be either uncontrolled or controlled. The mere fact that it calls useState() is not enough for us to know which one it is. Instead, we have to look at how its used.

When a component renders from internal state, its behaving as an uncontrolled component. And when it renders from props, it's being controlled by its parent. Which brings us back to our original question: is Counter uncontrolled or controlled?

It all depends on who's asking the question.

Last updated:

Get our latest in your inbox.

Join our newsletter to hear about Sam and Ryan's newest blog posts, code recipes, and videos.