Skip to content

Controllable State

hookstatecontrolleduncontrolled

Generic controlled/uncontrolled state hook. Manages value, defaultValue, and onChange, with a separate silent setter for restoring uncontrolled native form state.

tsx
const [value, setValue, isControlled, resetValue] = useControllableState({  value: props.value,  defaultValue: props.defaultValue ?? "",  onChange: props.onChange,});

Installation

$pnpm exec dgadd add ui/controllable-state
[Installs to]src/hooks/use-controllable-state.ts[Item]ui/controllable-state

dgadd ships in @diffgazer/add on npm: pnpm add -D @diffgazer/add puts it on pnpm exec, and npx @diffgazer/add runs it without the install.

UI components require Tailwind CSS v4. Local copy mode imports src/styles/styles.css; package mode uses @diffgazer/ui CSS from the installed package.

Parameters

NameTypeDefaultDescription
valueT | undefined—Controlled value. When provided, the component is in controlled mode.
defaultValuerequiredT—Initial value for uncontrolled mode.
onChange(value: T) => void—Called when the value changes, in both controlled and uncontrolled modes.
controlledboolean—Forces controlled-mode detection when value can be undefined but the consumer still owns state.

Returns

[T, (next: T | ((prev: T) => T)) => void, boolean, (next: T) => void]Tuple of [currentValue, setValue, isControlled, resetValue]. The public setter accepts direct values and updater functions; resetValue silently restores uncontrolled internal state.
NameTypeDefaultDescription
currentValuerequiredT—The current value, whether controlled or uncontrolled.
setValuerequired(next: T | ((prev: T) => T)) => void—State setter that works in both modes. Supports functional updates.
isControlledrequiredboolean—Whether the consumer is driving the value externally.
resetValuerequired(next: T) => void—Updates uncontrolled internal state without calling onChange. It is a no-op in controlled mode.

Examples

Basic Input

Preview

Toggle

Preview

Notes

When to Use

Use when building a component that should work both as controlled (value + onChange) and uncontrolled (defaultValue). It handles the state switching logic so the component doesn't need to.

Return Value

The isControlled flag reports external ownership. Use resetValue only for synchronization such as native form reset, where restoring an uncontrolled baseline must not emit a user change.

Source

Highlighted source loads after this disclosure opens. Browse the source repository.