useControllableState
State & dataThe controlled/uncontrolled state primitive every component library needs
Install
$ npm install @usefy/use-controllable-stateQuick start
useControllableState.tsx
import { useControllableState } from "@usefy/use-controllable-state";
// A switch that is controllable but works standalone.
function Switch({
checked,
defaultChecked,
onCheckedChange,
}: {
checked?: boolean;
defaultChecked?: boolean;
onCheckedChange?: (checked: boolean) => void;
}) {
const [on, setOn] = useControllableState({
value: checked,
defaultValue: defaultChecked ?? false,
onChange: onCheckedChange,
});
return (
<button role="switch" aria-checked={on} onClick={() => setOn((p) => !p)}>
{on ? "On" : "Off"}
</button>
);
}
// Uncontrolled — manages its own state:
<Switch defaultChecked />;
// Controlled — the parent owns the value:
const [value, setValue] = useState(false);
<Switch checked={value} onCheckedChange={setValue} />;API reference
const [value, setValue] = useControllableState<T>({
value, // T | undefined — the controlled value (defined ⇒ controlled)
defaultValue, // T | undefined — initial value used while uncontrolled
onChange, // ((value: T) => void) | undefined — called on every change
});
Options — UseControllableStateOptions<T>
| Option | Type | Description |
|---|---|---|
value | T | undefined | The controlled value. When not undefined, the hook is controlled: the returned value mirrors this prop and the setter only notifies onChange. |
defaultValue | T | undefined | Initial value used in uncontrolled mode (when value is undefined). Passed to useState, so a function value is treated as a lazy initializer. |
onChange | (value: T) => void | Called with the next value on every change. Its identity may change between renders without re-subscribing — the latest callback is always used. |
Returns — UseControllableStateReturn<T>
A readonly [value, setValue] tuple with the same shape as useState:
value: T— the effective value (the controlled prop when controlled, otherwise the internal state).setValue: Dispatch<SetStateAction<T>>— accepts a next value or an updater(prev) => next. In uncontrolled mode it updates internal state and firesonChangeon change; in controlled mode it only firesonChange(when the resolved value differs), leaving the parent to updatevalue.
Behavior notes
- Mode is per-render — it is decided each render by whether
value === undefined. Switching between defined/undefined switches modes; components should avoid doing so mid-lifecycle (same caveat as React's own controlled inputs). - No
onChangeon mount — it fires only when the value actually changes (compared withObject.is). - StrictMode-safe — in uncontrolled mode
onChangeis dispatched from an effect after commit, never from inside asetStateupdater, so it does not double-fire.
Go deeper
This page is the quick reference. For every example, prop, and edge case, read the full README — or open Storybook to change props live.