Support both controlled and uncontrolled usage. Value prop or internal state.
import { useState, useCallback, SetStateAction, Dispatch } from 'react';
export function useControlledState<T>(
value: T | undefined,
onChange: ((v: T) => void) | undefined,
defaultValue: T
): [T, Dispatch<SetStateAction<T>>] {
const [internal, setInternal] = useState<T>(defaultValue);
const isControlled = value !== undefined;
const current = isControlled ? value : internal;
const setValue = useCallback<Dispatch<SetStateAction<T>>>(
(next) => {
const nextValue = typeof next === 'function' ? (next as (prev: T) => T)(current) : next;
if (!isControlled) setInternal(nextValue);
onChange?.(nextValue);
},
[isControlled, current, onChange]
);
return [current, setValue];
}Pass value + onChange for controlled; omit value (undefined) and pass defaultValue for uncontrolled. Same pattern as native input.
Build a custom input that works both ways.
const [open, setOpen] = useControlledState(props.open, props.onOpenChange, false);