useControllableState
Powers every component in this registry that supports both a value/onValueChange (controlled) and a defaultValue (uncontrolled) mode — including Price Input, Elastic Slider, and Elastic Range Slider.
Last updated August 9, 2026
Overview
The first counter manages its own state internally (uncontrolled). The second is driven entirely by the parent (controlled) — the same Counter implementation handles both.
Uncontrolled — Counter owns its own state
3
Controlled — parent state is 5
5
"use client"
import * as React from "react"
import { Button } from "@/components/ui/button"
import { useControllableState } from "@/hooks/use-controllable-state"
function Counter({
value,
defaultValue = 0,
onValueChange,
}: {
value?: number
defaultValue?: number
onValueChange?: (value: number) => void
}) {
const [count, setCount] = useControllableState({
prop: value,
defaultProp: defaultValue,
onChange: onValueChange,
caller: "Counter",
})
return (
<div className="flex items-center gap-3">
<Button
variant="outline"
size="icon-sm"
onClick={() => setCount((current) => current - 1)}
>
-
</Button>
<span className="w-6 text-center font-mono text-sm">{count}</span>
<Button
variant="outline"
size="icon-sm"
onClick={() => setCount((current) => current + 1)}
>
+
</Button>
</div>
)
}
export function UseControllableStateDemoExample() {
const [controlledValue, setControlledValue] = React.useState(5)
return (
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-2">
<span className="text-xs text-muted-foreground">
Uncontrolled — Counter owns its own state
</span>
<Counter defaultValue={3} />
</div>
<div className="flex flex-col gap-2">
<span className="text-xs text-muted-foreground">
Controlled — parent state is {controlledValue}
</span>
<Counter value={controlledValue} onValueChange={setControlledValue} />
</div>
</div>
)
}Installation
$ npx shadcn@latest add https://ui.persian-labs.ir/r/use-controllable-state.jsonUsage
import { useControllableState } from "@/hooks/use-controllable-state"
function Counter({ value, defaultValue = 0, onValueChange }) {
const [count, setCount] = useControllableState({
prop: value,
defaultProp: defaultValue,
onChange: onValueChange,
caller: "Counter",
})
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>
}