useControllableState
useControllableState lets a single component support both controlled and
uncontrolled use from one hook. When the caller passes a value, the component is
controlled and the value is the single source of truth; when they pass only a
default, the component manages its own state. Either way, the setter is stable
across renders, so you can list it in effect dependencies without re-subscribing.
Signature
import { useControllableState } from '@hono-preact/ui';
function useControllableState<T>(opts: {
value?: T; // controlled; when defined, the component is controlled
defaultValue: T; // uncontrolled initial value
onChange?: (value: T) => void;
}): [T, (next: T) => void];
When value is defined the hook is controlled: it reads value and never
updates internal state, so the parent must apply the change. When value is
absent it is uncontrolled, seeded from defaultValue. The setter always calls
onChange.
Options
| Option | Type | Description |
|---|---|---|
value | T | Controlled value. When defined, the hook is controlled. |
defaultValue | T | Initial value when uncontrolled. |
onChange | (value: T) => void | Called with the next value on every change. |
Returns [value, setValue]: the current value and a stable setter.
Example
A toggle that works controlled or uncontrolled, mirroring how Dialog.Root
handles open / defaultOpen / onOpenChange:
import { useControllableState } from '@hono-preact/ui';
type ToggleProps = {
pressed?: boolean; // controlled
defaultPressed?: boolean; // uncontrolled
onPressedChange?: (pressed: boolean) => void;
};
export function Toggle({
pressed,
defaultPressed,
onPressedChange,
}: ToggleProps) {
const [on, setOn] = useControllableState<boolean>({
value: pressed,
defaultValue: defaultPressed ?? false,
onChange: onPressedChange,
});
return (
<button type="button" aria-pressed={on} onClick={() => setOn(!on)}>
{on ? 'On' : 'Off'}
</button>
);
}
// Uncontrolled: the component owns the state.
<Toggle defaultPressed onPressedChange={(p) => console.log(p)} />;
// Controlled: you own the state, the component reflects it.
const [pressed, setPressed] = useState(false);
<Toggle pressed={pressed} onPressedChange={setPressed} />;
The mode is assumed fixed for the component's lifetime, switching controlled and uncontrolled does not re-seed internal state, which matches typical usage.