useControllableState
Pre-alphaThe registry and the CLI are not published yet.Roadmap

useControllableState

State that works controlled or uncontrolled, with the value / defaultValue / onChange pattern.

Every Axiom input can be controlled (value + onValueChange) or manage its own state (defaultValue). useControllableState holds that logic once, for Switch, Checkbox, Slider, Accordion, Dialog and the other components with a value or an open state.

Installation

npx axiom add use-controllable-state
pnpm dlx axiom add use-controllable-state
yarn dlx axiom add use-controllable-state
bun x axiom add use-controllable-state

Components that need it copy it for you.

Usage

import { useControllableState } from "@/hooks/use-controllable-state";

type RatingProps = {
	value?: number;
	defaultValue?: number;
	onValueChange?: (value: number) => void;
};

function Rating({ value, defaultValue = 0, onValueChange }: RatingProps) {
	const [rating, setRating] = useControllableState({
		value,
		defaultValue,
		onChange: onValueChange,
	});

	return <Stars value={rating} onPress={setRating} />;
}

Both ways work for the person using the component:

<Rating defaultValue={3} />                        // uncontrolled
<Rating value={rating} onValueChange={setRating} /> // controlled

API

const [value, setValue] = useControllableState<T>({
	value,
	defaultValue,
	onChange,
});
OptionTypeDescription
valueTControlled value. When it’s set, the owner decides: setValue only calls onChange.
defaultValueTRequired. Initial value when value is undefined.
onChange(value: T) => voidCalled when setValue receives a different value (Object.is).
ReturnsTypeDescription
valueTvalue when controlled, the internal state otherwise.
setValue(next: T) => voidUpdates the internal state when uncontrolled, and calls onChange when the value changes.

undefined means uncontrolled. To control a value that can be empty, use null or '' for the empty state, not undefined.

A component shouldn’t switch between controlled and uncontrolled while mounted. defaultValue is only read on the first render.