/// hooks
useControllableState
The single source of the controlled/uncontrolled behaviour every PDS input shares. Pass value to control the component; omit it and pass defaultValue to let the hook own the state. Either way, onChange fires on every change.
Signature
function useControllableState<T>(params: {
value?: T;
defaultValue?: T;
onChange?: (value: T) => void;
}): [T, (next: T | ((prev: T) => T)) => void];Parameters
| Parameter | Type | Description |
|---|---|---|
value | T | undefined | The controlled value. When defined, the component is controlled. |
defaultValue | T | undefined | The initial value in uncontrolled mode. |
onChange | (value: T) => void | Called whenever the value changes, in both modes. |
Returns
A [value, setValue] tuple, like useState. setValue accepts a value or an updater.
| Field | Type | Description |
|---|---|---|
[0] value | T | The current value — the controlled prop when provided, else internal state. |
[1] setValue | (next) => void | Sets the value. In controlled mode it skips internal state and only fires onChange. |
When to use
- Building your own input, toggle, or disclosure component that should support both controlled and uncontrolled use without duplicating logic.
- Matching the PDS API convention (
value/defaultValue/onValueChange) so your component composes like the built-ins. - You do not need it for a purely controlled or purely uncontrolled component — plain
useStateis simpler there.
Example
import { useControllableState } from "@protocore/pds";
function Toggle({ pressed, defaultPressed, onPressedChange }) {
const [on, setOn] = useControllableState({
value: pressed,
defaultValue: defaultPressed ?? false,
onChange: onPressedChange,
});
return (
<button type="button" aria-pressed={on} onClick={() => setOn((p) => !p)}>
{on ? "On" : "Off"}
</button>
);
}Note
In controlled mode the hook never writes internal state — your parent’s
value is the single source of truth, and setValue only calls onChange. Re-render with the new prop to reflect the change.