All questions

useControllableValue

Premium

useControllableValue

A component is controlled when the value it shows lives in its parent and arrives as a prop, and uncontrolled when the component owns that value itself. React's own <input> is both, and it works out which by looking at its props: hand it a value and you are driving, hand it only a defaultValue and it drives itself. Every component library ships this duality, because a picker that only does one of the two is a picker half its users cannot adopt. useControllableValue(props) is that switch, in one hook.

Signature

function useControllableValue<T>(props?: {
  value?: T;                     // passed -> controlled: the parent owns the value
  defaultValue?: T;              // seeds the hook's own state when uncontrolled
  onChange?: (value: T) => void; // called on every setValue, in BOTH modes
}): [T, (next: T | ((current: T) => T)) => void];

props may be omitted entirely. setValue keeps one identity for the life of the component.

Examples

Uncontrolled — no value prop, so the hook owns it. setValue moves the value and tells the parent it happened:

const [value, setValue] = useControllableValue({ defaultValue: 'a', onChange: log });

setValue('b'); // value is 'b' on the next render, and log('b') fired

Controlled — a value prop, so the parent owns it. setValue does not move the value; it asks:

const [value, setValue] = useControllableValue({ value: 'a', onChange: log });

setValue('b'); // value is STILL 'a'. log('b') fired — it is the parent's move now.

Wired to a real parent, that request comes back around as a new prop:

function NameField(props) {
  const [value, setValue] = useControllableValue(props);
  return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}

function Parent() {
  const [name, setName] = useState('Ada');
  return <NameField value={name} onChange={setName} />; // controlled
}

function Standalone() {
  return <NameField defaultValue="Ada" />; // same component, uncontrolled
}

Notes

  • The mode comes from the props, not from a flag. There is no controlled: true argument. The hook works out which mode it is in from what it was handed, the same way <input> does.
  • Falsy values are values. 0 on a number input, '' on a field the user just cleared, false on a checkbox — a parent passing any of these is controlling the component, and the hook has to keep answering to it.
  • In controlled mode setValue does not change what the hook returns. It calls onChange and stops. If the parent ignores onChange, the value never changes. That is the contract, not a bug.
  • onChange fires in both modes, with the resolved value, and is optional.
  • setValue takes an updater, like useState's setter. setValue(v => v + 1) resolves against the current value — which in controlled mode is the prop.
  • defaultValue seeds once. Like useState, a later defaultValue does not move a value the hook already owns.

Unlock the solution & editor

  • Runnable editor + tests

    Solve in the browser with instant Jest feedback.

  • Detailed solutions

    Walkthroughs, edge cases, and complexity notes.

  • Multi-framework variants

    React, Vue, Vanilla, Angular — same question, different stacks.

Upgrade to Premium