aboutsummaryrefslogtreecommitdiff
path: root/packages/ui/hooks/use-controllable-state.ts
blob: 0677fe18ad73bbe4cf0b071ad70ef08e37a1e9d5 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import * as React from "react";

import { useCallbackRef } from "@repo/ui/hooks/use-callback-ref";

/**
 * @see https://github.com/radix-ui/primitives/blob/main/packages/react/use-controllable-state/src/useControllableState.tsx
 */

type UseControllableStateParams<T> = {
	prop?: T | undefined;
	defaultProp?: T | undefined;
	onChange?: (state: T) => void;
};

type SetStateFn<T> = (prevState?: T) => T;

function useControllableState<T>({
	prop,
	defaultProp,
	onChange = () => {},
}: UseControllableStateParams<T>) {
	const [uncontrolledProp, setUncontrolledProp] = useUncontrolledState({
		defaultProp,
		onChange,
	});
	const isControlled = prop !== undefined;
	const value = isControlled ? prop : uncontrolledProp;
	const handleChange = useCallbackRef(onChange);

	const setValue: React.Dispatch<React.SetStateAction<T | undefined>> =
		React.useCallback(
			(nextValue) => {
				if (isControlled) {
					const setter = nextValue as SetStateFn<T>;
					const value =
						typeof nextValue === "function" ? setter(prop) : nextValue;
					if (value !== prop) handleChange(value as T);
				} else {
					setUncontrolledProp(nextValue);
				}
			},
			[isControlled, prop, setUncontrolledProp, handleChange],
		);

	return [value, setValue] as const;
}

function useUncontrolledState<T>({
	defaultProp,
	onChange,
}: Omit<UseControllableStateParams<T>, "prop">) {
	const uncontrolledState = React.useState<T | undefined>(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 { useControllableState };