Skip to content

Commit 7b22596

Browse files
committed
feat: 🚀 useControlledState
1 parent 0365c72 commit 7b22596

File tree

7 files changed

+236
-12
lines changed

7 files changed

+236
-12
lines changed

docs/.vitepress/config/en.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ export function sidebarHooks(): DefaultTheme.SidebarItem[] {
197197
text: 'State',
198198
items: [
199199
{ text: 'useBoolean', link: 'useBoolean' },
200-
// { text: "useControlledState", link: "useControlledState" },
200+
{ text: "useControlledState", link: "useControlledState" },
201201
{ text: 'useImmer', link: 'useImmer' },
202202
{ text: 'useUrlState', link: 'useUrlState' },
203203
{ text: 'useFormatResult', link: 'useFormatResult' },
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<template>
2+
<div>
3+
<div>Show default value: {{ count }}</div>
4+
<div>Show form value: {{ countFormValue }}</div>
5+
<vhp-button @click="() => (value = 10)">Set value</vhp-button>
6+
</div>
7+
</template>
8+
9+
<script lang="ts" setup>
10+
import { ref } from 'vue'
11+
import { useControlledState } from 'vue-hooks-plus'
12+
const value = ref<number | undefined>(undefined)
13+
const [count] = useControlledState(undefined, 10)
14+
const [countFormValue] = useControlledState(value, 0)
15+
</script>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<template>
2+
<div>
3+
<vhp-button
4+
:style="{ backgroundColor: color }"
5+
@click="
6+
() => {
7+
setColor(randomColor())
8+
}
9+
"
10+
>Uncontrolled color</vhp-button
11+
>
12+
</div>
13+
</template>
14+
15+
<script lang="ts" setup>
16+
import { useControlledState } from 'vue-hooks-plus'
17+
const [color, setColor] = useControlledState(undefined, 'red')
18+
const randomColor = () => {
19+
return '#' + Math.floor(Math.random() * 16777215).toString(16)
20+
}
21+
</script>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<template>
2+
<div>
3+
<vhp-button
4+
style="margin-right: 10px"
5+
:style="{ backgroundColor: color1 }"
6+
@click="
7+
() => {
8+
setColor1(randomColor())
9+
}
10+
"
11+
>Controlled unvalid value</vhp-button
12+
>
13+
14+
<vhp-button
15+
:style="{ backgroundColor: color }"
16+
@click="
17+
() => {
18+
setColor(randomColor())
19+
}
20+
"
21+
>Controlled color</vhp-button
22+
>
23+
</div>
24+
</template>
25+
26+
<script lang="ts" setup>
27+
import { ref } from 'vue'
28+
import { useControlledState } from 'vue-hooks-plus'
29+
30+
const value = ref<string | undefined>('red')
31+
32+
const value2 = ref<string | undefined>('red')
33+
34+
const [color1, setColor1] = useControlledState(value2, undefined)
35+
const [color, setColor] = useControlledState(value, undefined, v => {
36+
value.value = v
37+
})
38+
const randomColor = () => {
39+
return '#' + Math.floor(Math.random() * 16777215).toString(16)
40+
}
41+
</script>
Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,72 @@
1-
# useControlledState `^2.3.0`
1+
---
2+
map:
3+
# Path mapped to docs
4+
path: /useControlledState
5+
---
6+
7+
# useControlledState `^2.4.0`
8+
9+
A hook for managing controlled and uncontrolled state, similar to React's controlled/uncontrolled pattern.
10+
11+
<!-- [[toc]] -->
12+
13+
## Default Value Mode
14+
15+
When the state is empty, the default value will be used.
16+
17+
<demo src="useControlledState/demo.vue"
18+
language="vue"
19+
title="Basic usage"
20+
desc="Switch between controlled and uncontrolled state."> </demo>
21+
22+
## Uncontrolled Mode
23+
24+
If the first argument does not pass a reactive value, useControlledState will maintain an internally managed value that is not controlled externally.
25+
26+
<demo src="useControlledState/demo1.vue"
27+
language="vue"
28+
title="Uncontrolled state"
29+
desc=""> </demo>
30+
31+
## Controlled Mode
32+
33+
If the first argument passes a reactive value, the state inside useControlledState will be fully controlled by the outside.
34+
35+
You need to update the reactive value of the first argument in the onChange callback for the change to take effect.
36+
37+
<demo src="useControlledState/demo2.vue"
38+
language="vue"
39+
title="Controlled state"
40+
desc=""></demo>
41+
42+
## API
43+
44+
```typescript
45+
function useControlledState<T, C = T>(
46+
value?: Ref<T | undefined>,
47+
defaultValue: T,
48+
onChange?: (v: C, ...args: any[]) => void
49+
): [ComputedRef<T>, (value: T | ((prev: T) => T), ...args: any[]) => void]
50+
```
51+
52+
## Params
53+
54+
| Property | Description | Type | Default |
55+
| ------------ | --------------------------------------------------------------------------- | --------------------------- | --------- |
56+
| value | Controlled value, if `undefined` will use internal state | `Ref<T | undefined>` | - |
57+
| defaultValue | Default value when uncontrolled | `T` | - |
58+
| onChange | Callback when value changes (called for both controlled and uncontrolled) | `(v: C, ...args: any[]) => void` | `undefined` |
59+
60+
## Result
61+
62+
| Property | Description | Type |
63+
| ---------- | --------------------------------------------- | ----------------------------------------- |
64+
| state | Current value (computed, always in sync) | `ComputedRef<T>` |
65+
| setState | Update value (will call onChange if changed) | `(value: T | ((prev: T) => T), ...args: any[]) => void` |
66+
67+
## Remarks
68+
69+
- If `value` is provided (not `undefined`), the state is controlled by the parent, and `setState` will only trigger `onChange` but not update the value directly.
70+
- If `value` is `undefined`, the state is uncontrolled and managed internally.
71+
- If you use a function as the argument to `setState`, a warning will be shown (for compatibility with React API, but not recommended in Vue).
72+
- The hook will warn if you switch between controlled and uncontrolled mode during the component lifecycle.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
map:
3+
# Path mapped to docs
4+
path: /useControlledState
5+
---
6+
7+
# useControlledState `^2.4.0`
8+
9+
一个用于管理受控和非受控状态的 Hook,类似于 React 的受控/非受控模式。
10+
11+
<!-- [[toc]] -->
12+
13+
## 默认值模式
14+
15+
当状态为空的时候,将使用默认值。
16+
17+
<demo src="useControlledState/demo.vue"
18+
language="vue"
19+
title="基本用法"
20+
desc="默认值会在值为非法的时候应用"> </demo>
21+
22+
## 非受控模式
23+
24+
如果第一个参数不传递响应式值,useControlledState 将在内部维护一个不受外部控制的值。
25+
26+
<demo src="useControlledState/demo1.vue"
27+
language="vue"
28+
title="非受控模式"
29+
desc=""> </demo>
30+
31+
## 受控模式
32+
33+
如果第一个参数传递响应式值,useControlledState 内的状态将完全由外部控制。
34+
35+
需要在第三个参数 onChange 时将第一个参数的响应式值变更才会生效。
36+
37+
<demo src="useControlledState/demo2.vue"
38+
language="vue"
39+
title="受控模式"
40+
desc=""></demo>
41+
42+
## API
43+
44+
```typescript
45+
function useControlledState<T, C = T>(
46+
value?: Ref<T | undefined>,
47+
defaultValue: T,
48+
onChange?: (v: C, ...args: any[]) => void
49+
): [ComputedRef<T>, (value: T | ((prev: T) => T), ...args: any[]) => void]
50+
```
51+
52+
## 参数说明
53+
54+
| 参数 | 说明 | 类型 | 默认值 |
55+
| ------------ | ---------------------------------------------------------------------- | --------------------------- | -------- |
56+
| value | 受控值,如果为 `undefined` 则使用内部状态 | `Ref<T | undefined>` | - |
57+
| defaultValue | 非受控时的默认值 | `T` | - |
58+
| onChange | 值变化时的回调(受控和非受控都会调用) | `(v: C, ...args: any[]) => void` | `undefined` |
59+
60+
## 返回结果
61+
62+
| 属性 | 说明 | 类型 |
63+
| ---------- | -------------------------------------------- | ----------------------------------------- |
64+
| state | 当前值(computed,始终保持同步) | `ComputedRef<T>` |
65+
| setState | 更新值(变更时会调用 onChange| `(value: T | ((prev: T) => T), ...args: any[]) => void` |
66+
67+
## 备注
68+
69+
- 如果传入了 `value`(不是 `undefined`),则状态由父组件控制,`setState` 只会触发 `onChange`,不会直接更新值。
70+
- 如果 `value``undefined`,则状态为非受控,由内部管理。
71+
- 如果 `setState` 的参数为函数,会有警告(为兼容 React API,不推荐在 Vue 中使用)。
72+
- 如果在组件生命周期内切换了受控和非受控模式,会有警告。

packages/hooks/src/useControlledState/index.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
1-
import { ref, computed, watch, type Ref, type ComputedRef } from 'vue';
1+
import { ref, computed, watch, type Ref, type ComputedRef, watchEffect } from 'vue';
22

33
export function useControlledState<T, C = T>(
4-
value: Ref<T | undefined>,
5-
defaultValue: T,
4+
value?: Ref<T | undefined>,
5+
defaultValue?: T,
66
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;
7+
): [ComputedRef<T | undefined>, (value: T | ((prev: T) => T), ...args: any[]) => void] {
8+
const isControlled = computed(() => value?.value !== undefined);
9+
const initialValue = value?.value ?? defaultValue;
1010
const internalState = ref(initialValue) as Ref<T>;
1111
const wasControlled = ref(isControlled.value);
1212

1313
const currentValue = computed(() =>
14-
isControlled.value ? value.value! : internalState.value
14+
isControlled.value ? value?.value : internalState.value
1515
);
1616

17+
watchEffect(() => {
18+
console.log("isControlled", isControlled.value);
19+
})
20+
1721
watch(isControlled, (newVal, oldVal) => {
1822
if (newVal !== oldVal) {
1923
console.warn(
@@ -24,13 +28,14 @@ export function useControlledState<T, C = T>(
2428
}
2529
});
2630

31+
2732
function setValue(newValue: T | ((prev: T) => T), ...args: any[]) {
2833
if (typeof newValue === 'function') {
2934
console.warn(
30-
'Function callbacks are not supported. See: https://github.com/adobe/react-spectrum/issues/2320'
35+
'Function callbacks are not supported.'
3136
);
3237
const prev = currentValue.value;
33-
const updatedValue = (newValue as (prev: T) => T)(prev);
38+
const updatedValue = (newValue as (prev?: T) => T)(prev);
3439

3540
if (!isControlled.value) {
3641
internalState.value = updatedValue;
@@ -41,7 +46,6 @@ export function useControlledState<T, C = T>(
4146
}
4247
} else {
4348
const shouldUpdate = !Object.is(currentValue.value, newValue);
44-
4549
if (!isControlled.value) {
4650
internalState.value = newValue;
4751
}

0 commit comments

Comments
 (0)