useControlledState
useControlledState is the canonical pattern for components that accept either a value prop (controlled by the parent) or only a defaultValue (managed internally). It mirrors the controlled / uncontrolled split that React itself uses for native form elements like <input>.
Live preview
Import
import { useControlledState } from 'entangle-ui';Signature
function useControlledState<T>( options: UseControlledStateOptions<T>): [T, (next: T | ((prev: T) => T)) => void];
interface UseControlledStateOptions<T> { value?: T; defaultValue?: T; onChange?: (value: T) => void; fallback: T;}Usage
Wire the hook through every “controllable” prop on your component:
function Toggle({ value, defaultValue, onChange }) { const [on, setOn] = useControlledState({ value, defaultValue, onChange, fallback: false, });
return ( <button onClick={() => setOn(prev => !prev)}>{on ? 'On' : 'Off'}</button> );}Controlled
When the parent passes value, the hook returns that value verbatim and never mutates internal state. setValue becomes a pure side-effect that calls onChange.
Controlled
Uncontrolled
When value is undefined, the hook owns the state. defaultValue is the initial value; setValue updates internal state and calls onChange (if provided).
Uncontrolled
Returns
A [value, setValue] tuple, like useState. setValue accepts either a next value or a functional updater (prev) => next.
API
| Prop | Type | Default | Description |
|---|---|---|---|
value | T | — | When defined, drives the returned value. Switching this from defined to undefined (or vice versa) at runtime triggers a development warning. |
defaultValue | T | — | Initial value used in uncontrolled mode. |
onChange | (value: T) => void | — | Called when the consumer requests a change. Fires in both controlled and uncontrolled modes. |
fallback * | T | — | Value used when both value and defaultValue are undefined. Required so the hook never returns undefined. |
Common pitfalls
- Switching modes mid-life: A component should be either controlled or uncontrolled for its entire lifetime. The hook emits a development-only warning if
valueflips between defined and undefined after mount, mirroring React’s own warning for<input>. - Forgetting
fallback:fallbackis required precisely so consumers do not getundefinedfrom the hook when the parent forgets to supply bothvalueanddefaultValue. Always pass an explicit fallback that matches the typeT. - Stale
onChange: The hook keeps an internal ref toonChange, so passing a fresh inline function on every render is safe — the observer is not re-subscribed.