hono-preact

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

OptionTypeDescription
valueTControlled value. When defined, the hook is controlled.
defaultValueTInitial value when uncontrolled.
onChange(value: T) => voidCalled 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.