Skip to main content

Function: useControlledState()

useControlledState<T>(value, defaultValue, onChange?): readonly [T, (newValue) => void]

Defined in: use-controlled-state.ts:90

A custom React hook that implements the controlled/uncontrolled component pattern.

This hook allows components to work in two modes:

  • Controlled mode: When value is provided, the parent component manages the state
  • Uncontrolled mode: When value is undefined, the hook manages state internally

This pattern provides flexibility for component consumers, allowing them to choose between full control of the state or letting the component manage it automatically.

The setValue function is optimized for performance with a stable reference that only changes when the component switches between controlled/uncontrolled modes or when the onChange callback changes. This prevents unnecessary re-renders in child components.

In development mode, the hook will warn if a component switches between controlled and uncontrolled modes, which is considered an anti-pattern and can lead to unexpected behavior.

Type Parameters​

T​

T

The type of the state value.

Parameters​

value​

T | undefined

The controlled value. When provided, the hook operates in controlled mode.

defaultValue​

T | (() => T)

The initial value for uncontrolled mode. Can be a value or lazy initializer function.

onChange?​

(value) => void

Optional callback invoked whenever the value changes.

Returns​

readonly [T, (newValue) => void]

A tuple containing the current value and a stable setter function that accepts either a new value or an updater function.

Examples​

// Uncontrolled usage - component manages its own state
function UncontrolledInput() {
const [value, setValue] = useControlledState(undefined, "", console.log);
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}
// Controlled usage - parent manages the state
function ControlledInput({ value, onChange }) {
const [inputValue, setInputValue] = useControlledState(value, "", onChange);
return <input value={inputValue} onChange={(e) => setInputValue(e.target.value)} />;
}
// With updater function for complex state updates
const [count, setCount] = useControlledState(undefined, 0);
setCount(prev => prev + 1);
// With lazy initialization for expensive defaults
const [data, setData] = useControlledState(
undefined,
() => expensiveComputation()
);