Founding price: $35, then $99 after 23:59 UTC on 4 November, then $99
Get ProState that works controlled or uncontrolled: pass value to control it, or defaultValue to let the hook own it, and onChange fires either way.
This item has no props to tune. Use the lens above to change theme, density, direction and more.
Use it when you build a component that should work both ways, like an input, a tabs list or a disclosure: the caller can pass value and onChange to own the state, or defaultValue (or nothing) to let the component keep it.
Do not use it for plain local state in an app screen; useState is enough there.
import { useControllableState } from "@/hooks/use-controllable-state"function Disclosure({ open, defaultOpen = false, onOpenChange }: Props) {
const [isOpen, setOpen] = useControllableState({
value: open,
defaultValue: defaultOpen,
onChange: onOpenChange,
})
return <button onClick={() => setOpen((v) => !v)}>{String(isOpen)}</button>
}useControllableState<T>(options): [T, (next: T | ((previous: T) => T)) => void]
| Option | Type | Description |
|---|---|---|
value | T | undefined | When not undefined the state is controlled by the caller. |
defaultValue | T | Initial value when uncontrolled. |
onChange | (value: T) => void | Called with the next value in both modes, and only when it changes. |
In controlled mode the setter never changes the value itself; it only calls onChange. The setter keeps the same identity between renders.
A hook has no markup. Controlled and uncontrolled modes must expose the same ARIA state (for example aria-expanded), so derive it from the returned value.
The hook ships with renderHook tests (use-controllable-state.test.ts) covering its behaviour, cleanup on unmount and server-safe defaults. It is safe to render on the server.