// This code comes from https://github.com/radix-ui/primitives/blob/main/packages/react/use-controllable-state/src/useControllableState.tsx import React from 'react'; type UseControllableStateParams = { prop?: T | undefined; defaultProp?: T | undefined; onChange?: (state: T) => void; }; type SetStateFn = (prevState?: T) => T; function useCallbackRef any>(callback: T | undefined): T { const callbackRef = React.useRef(callback); React.useEffect(() => { callbackRef.current = callback; }); // https://github.com/facebook/react/issues/19240 return React.useMemo(() => ((...args) => callbackRef.current?.(...args)) as T, []); } function useUncontrolledState({ defaultProp, onChange, }: Omit, 'prop'>) { const uncontrolledState = React.useState(defaultProp); const [value] = uncontrolledState; const prevValueRef = React.useRef(value); const handleChange = useCallbackRef(onChange); React.useEffect(() => { if (prevValueRef.current !== value) { handleChange(value as T); prevValueRef.current = value; } }, [value, prevValueRef, handleChange]); return uncontrolledState; } export function useControllableState({ prop, defaultProp, onChange = () => {}, }: UseControllableStateParams) { const [uncontrolledProp, setUncontrolledProp] = useUncontrolledState({ defaultProp, onChange }); const isControlled = prop !== undefined; const value = isControlled ? prop : uncontrolledProp; const handleChange = useCallbackRef(onChange); const setValue: React.Dispatch> = React.useCallback( (nextValue) => { if (isControlled) { const setter = nextValue as SetStateFn; const next = typeof nextValue === 'function' ? setter(prop) : nextValue; if (next !== prop) handleChange(next as T); } else { setUncontrolledProp(nextValue); } }, [isControlled, prop, setUncontrolledProp, handleChange] ); return [value, setValue] as const; }