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/onChangefor the controlled pair, matching the built-in inputs.defaultValuefor the uncontrolled starting point.onSomethingChangefor named values, such asonOpenChange.
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.