Chapter 4 of 4

Controlled and Uncontrolled Components

Designing a component that works both ways, the way the built-in inputs do.

The same distinction that applies to form inputs applies to your own components. A controlled component takes its value from props; an uncontrolled one manages its own state internally.

// Uncontrolled: the caller does not care about the value
<Accordion defaultOpen="billing" />

// Controlled: the caller owns the value
<Accordion open={openSection} onOpenChange={setOpenSection} />

Supporting both

The standard trick: use the prop when it is provided, and fall back to internal state when it is not.

function Accordion({ open, defaultOpen, onOpenChange, children }) {
  const [internal, setInternal] = useState(defaultOpen);

  const isControlled = open !== undefined;
  const value = isControlled ? open : internal;

  function change(next) {
    if (!isControlled) setInternal(next);
    onOpenChange?.(next);
  }

  // ...render using value and change
}

Naming conventions worth following

  • value / onChange for the controlled pair, matching the built-in inputs.
  • defaultValue for the uncontrolled starting point.
  • onSomethingChange for named values, such as onOpenChange.

What you have learned

  • Composition and slots solve most reuse problems without a named pattern.
  • Compound components share state through context and let the caller own the markup.
  • Render props and HOCs are largely superseded by custom hooks, but remain in libraries and older code.
  • Supporting controlled and uncontrolled use makes a component usable in both simple and complex situations.

Pick the effective value of a dual-mode component

Complete resolveValue so it returns the controlled value when it is not undefined, and the internal value otherwise.