Skip to main content
useControllableState is a hook that manages both controlled and uncontrolled state patterns, allowing components to work flexibly in either mode. It handles the complexity of switching between controlled and uncontrolled modes and provides warnings in development when incorrect patterns are detected.

Installation

npm install @radix-ui/react-use-controllable-state

Function Signature

function useControllableState<T>({
  prop,
  defaultProp,
  onChange,
  caller,
}: UseControllableStateParams<T>): [T, SetStateFn<T>]

Parameters

prop
T | undefined
The controlled value from props. When provided, the component operates in controlled mode.
defaultProp
T
required
The default value for uncontrolled mode. This is required and serves as the initial state when prop is undefined.
onChange
(state: T) => void
Callback function invoked when the state changes. Called in both controlled and uncontrolled modes.Default: () => {}
caller
string
Component name for debugging purposes. Used in development warnings when the component switches between controlled and uncontrolled modes.

Return Value

value
T
The current state value. Either the controlled prop value or the internal uncontrolled state.
setValue
React.Dispatch<React.SetStateAction<T>>
Function to update the state. Handles both controlled and uncontrolled patterns automatically.

Usage

Basic Example

import { useControllableState } from '@radix-ui/react-use-controllable-state';

interface ToggleProps {
  value?: boolean;
  defaultValue?: boolean;
  onValueChange?: (value: boolean) => void;
}

function Toggle({ value: valueProp, defaultValue = false, onValueChange }: ToggleProps) {
  const [value, setValue] = useControllableState({
    prop: valueProp,
    defaultProp: defaultValue,
    onChange: onValueChange,
    caller: 'Toggle',
  });

  return (
    <button onClick={() => setValue((prev) => !prev)}>
      {value ? 'On' : 'Off'}
    </button>
  );
}

Controlled Mode

function App() {
  const [isOn, setIsOn] = useState(false);
  
  return (
    <Toggle 
      value={isOn} 
      onValueChange={setIsOn}
    />
  );
}

Uncontrolled Mode

function App() {
  return (
    <Toggle 
      defaultValue={false}
      onValueChange={(value) => console.log('Changed to:', value)}
    />
  );
}

Type Definitions

type ChangeHandler<T> = (state: T) => void;
type SetStateFn<T> = React.Dispatch<React.SetStateAction<T>>;

interface UseControllableStateParams<T> {
  prop?: T | undefined;
  defaultProp: T;
  onChange?: ChangeHandler<T>;
  caller?: string;
}

Notes

In development mode, the hook will warn you if a component switches between controlled and uncontrolled modes. This helps catch common bugs where state management patterns change unexpectedly.
The onChange callback is stored in a ref and updated using useInsertionEffect (or useLayoutEffect as a fallback) to ensure it doesn’t cause unnecessary re-renders when passed as a dependency.

Build docs developers (and LLMs) love