useControllableState

Naseebullah Ahmadi  Senior Software Engineer, London

One hook that lets a component be driven from outside (`value` + `onChange`) or manage its own state (`defaultValue`), the same two-mode contract a native `<input>` has, while the component body only ever reads one value and calls one setter.

4 min read
#frontend

A native <input> works two ways: pass value + onChange and you own its state, or pass defaultValue and it owns its own. useControllableState gives a custom component that same choice from a single hook - the body just reads one value and calls one setValue, and never has to branch on which mode it's in.

@itsnas color-picker.tsx
codecolor-picker.tsx
'use client'
 
interface ColorPickerProps {
  value?: string
  defaultValue?: string
  onChange?: (value: string) => void
}
 
function ColorPicker({
  value,
  defaultValue = '#000000',
  onChange,
}: ColorPickerProps) {
  // @src/code/controllable-state
  const [color, setColor] = useControllableState({
    value,
    defaultValue,
    onChange,
  })
 
  return (
    <input
      type="color"
      value={color}
      onChange={e => setColor(e.target.value)}
    />
  )
}
 
// Uncontrolled - ColorPicker keeps its own state, parent just
// hears about changes:
//   <ColorPicker defaultValue="#0ea5e9" onChange={save} />
//
// Controlled - parent owns the value:
//   <ColorPicker value={color} onChange={setColor} />
main
Nas (@itsnas)