useControllableState
Unified state for components that can be controlled or uncontrolled. When value is provided, the parent owns the state and the setter only fires onChange. When value is omitted, the hook keeps state internally and fires onChange on each change.
This is the same primitive Wire UI uses internally on every controllable component (Modal, Drawer, Tabs, Accordion, …) — exported so your own components can offer the same dual API.
import { useControllableState } from '@wire-ui/react'
interface SwitchProps {
checked?: boolean
defaultChecked?: boolean
onCheckedChange?: (checked: boolean) => void
}
function Switch({ checked, defaultChecked, onCheckedChange }: SwitchProps) {
const [isOn, setOn] = useControllableState({
value: checked,
defaultValue: defaultChecked ?? false,
onChange: onCheckedChange,
})
return (
<button
type="button"
role="switch"
aria-checked={isOn}
onClick={() => setOn(!isOn)}
>
{isOn ? 'On' : 'Off'}
</button>
)
}Behavior
- When
valueis provided (defined), the hook is controlled: writes do not mutate internal state — they only callonChange. The parent must react by updating its own state. - When
valueisundefined, the hook is uncontrolled: writes update internal state and callonChange. - Switching between controlled and uncontrolled across a component’s lifetime is unsupported.
Options
| Option | Type | Description |
|---|---|---|
value | T | undefined | When defined, makes the hook controlled. |
defaultValue | T | Initial value in uncontrolled mode. |
onChange | (value: T) => void | Called on every change in both modes. |
Returns
A[value, setValue] tuple, similar to useState. setValue accepts either a new value or an updater function.
Last updated on