Skip to content

Commit 6874ec6

Browse files
committed
feat: useControlledState
1 parent 2f52bff commit 6874ec6

File tree

2 files changed

+58
-0
lines changed

2 files changed

+58
-0
lines changed

packages/hooks/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import useAsyncOrder from './useAsyncOrder'
77
import useBoolean from './useBoolean'
88
import useCookieState from './useCookieState'
99
import useCounter from './useCounter'
10+
import { useControlledState } from './useControlledState'
1011
import useDebounce from './useDebounce'
1112
import useDebounceFn from './useDebounceFn'
1213
import useDrag from './useDrag'
@@ -62,6 +63,7 @@ export {
6263
useBoolean,
6364
useCookieState,
6465
useCounter,
66+
useControlledState,
6567
useDebounce,
6668
useDebounceFn,
6769
useDrag,
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { ref, computed, watch, type Ref, type ComputedRef } from 'vue';
2+
3+
export function useControlledState<T, C = T>(
4+
value: Ref<T | undefined>,
5+
defaultValue: T,
6+
onChange?: (v: C, ...args: any[]) => void
7+
): [ComputedRef<T>, (value: T | ((prev: T) => T), ...args: any[]) => void] {
8+
const isControlled = computed(() => value.value !== undefined);
9+
const initialValue = value.value ?? defaultValue;
10+
const internalState = ref(initialValue) as Ref<T>;
11+
const wasControlled = ref(isControlled.value);
12+
13+
const currentValue = computed(() =>
14+
isControlled.value ? value.value! : internalState.value
15+
);
16+
17+
watch(isControlled, (newVal, oldVal) => {
18+
if (newVal !== oldVal) {
19+
console.warn(
20+
`WARN: Component changed from ${wasControlled.value ? 'controlled' : 'uncontrolled'} ` +
21+
`to ${newVal ? 'controlled' : 'uncontrolled'}`
22+
);
23+
wasControlled.value = newVal;
24+
}
25+
});
26+
27+
function setValue(newValue: T | ((prev: T) => T), ...args: any[]) {
28+
if (typeof newValue === 'function') {
29+
console.warn(
30+
'Function callbacks are not supported. See: https://github.com/adobe/react-spectrum/issues/2320'
31+
);
32+
const prev = currentValue.value;
33+
const updatedValue = (newValue as (prev: T) => T)(prev);
34+
35+
if (!isControlled.value) {
36+
internalState.value = updatedValue;
37+
}
38+
39+
if (!Object.is(prev, updatedValue)) {
40+
onChange?.(updatedValue as unknown as C, ...args);
41+
}
42+
} else {
43+
const shouldUpdate = !Object.is(currentValue.value, newValue);
44+
45+
if (!isControlled.value) {
46+
internalState.value = newValue;
47+
}
48+
49+
if (shouldUpdate) {
50+
onChange?.(newValue as unknown as C, ...args);
51+
}
52+
}
53+
}
54+
55+
return [currentValue, setValue];
56+
}

0 commit comments

Comments
 (0)