useControllableState
A value that is controlled through a prop or kept internally, with one setter that updates either and reports the change. The pattern behind value and defaultValue.
Example
Installation
npx shadcn@latest add https://hirael.com/r/use-controllable-state.jsonUsage
import { useControllableState } from "@/hooks/use-controllable-state"Paths assume the default @/ alias from your components.json.
API
useControllableState()
| Parameter | Type | Default |
|---|---|---|
prop*The controlled value. `undefined` hands control to the internal state. | T | undefined | none |
defaultProp*Starting value while uncontrolled. | T | none |
onChangeFires on every change, controlled or not. | ((value: T) => void) | none |
Returns
readonly [T, (next: T) => void]Source
import * as React from 'react';
interface UseControllableStateParams<T> {
/** The controlled value. `undefined` hands control to the internal state. */
prop: T | undefined;
/** Starting value while uncontrolled. */
defaultProp: T;
// Method syntax keeps the parameter bivariant, so a handler typed for the non-empty value still fits.
/** Fires on every change, controlled or not. */
onChange?(value: T): void;
}
/** A value that is either controlled through `prop` or kept internally, with one setter for both. */
export const useControllableState = <T>({ prop, defaultProp, onChange }: UseControllableStateParams<T>) => {
const [uncontrolled, setUncontrolled] = React.useState(defaultProp);
const isControlled = prop !== undefined;
const value = isControlled ? prop : uncontrolled;
// Read through a ref so an inline `onChange` doesn't give the setter a new identity every render,
// which would defeat the memoized context values built on it.
const onChangeRef = React.useRef(onChange);
React.useLayoutEffect(() => {
onChangeRef.current = onChange;
});
const setValue = React.useCallback(
(next: T) => {
if (!isControlled) setUncontrolled(next);
onChangeRef.current?.(next);
},
[isControlled],
);
return [value, setValue] as const;
};
Shipped