3

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.json

Usage

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>
}

API Reference

useControllableState(options)