diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index 7ec0a971..a6a330bd 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -7,5 +7,5 @@ import useHookTable from './use-table'; export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useHookTable }; -export * from './use-state'; +export * from './use-signal'; export * from './use-table'; diff --git a/packages/hooks/src/use-signal.ts b/packages/hooks/src/use-signal.ts new file mode 100644 index 00000000..e07d786f --- /dev/null +++ b/packages/hooks/src/use-signal.ts @@ -0,0 +1,105 @@ +import { computed, shallowRef, triggerRef } from 'vue'; +import type { ComputedGetter, DebuggerOptions, ShallowRef, WritableComputedOptions, WritableComputedRef } from 'vue'; + +type Updater = (value: T) => T; +type Mutator = (value: T) => void; + +/** + * Signal is a reactive value that can be set, updated or mutated + * + * ```ts + * const count = useSignal(0); + * + * // `watchEffect` + * watchEffect(() => { + * console.log(count()); + * }); + * + * // watch + * watch(count, value => { + * console.log(value); + * }); + * + * // useComputed + * const double = useComputed(() => count() * 2); + * const writeableDouble = useComputed({ + * get: () => count() * 2, + * set: value => count.set(value / 2) + * }); + * ``` + */ +export interface Signal { + (): Readonly; + /** + * Set the value of the signal + * + * It recommend use `set` for primitive values + * + * @param value + */ + set(value: T): void; + /** + * Update the value of the signal using an updater function + * + * It recommend use `update` for non-primitive values, only the first level of the object will be reactive. + * + * @param updater + */ + update(updater: Updater): void; + /** + * Mutate the value of the signal using a mutator function + * + * this action will call `triggerRef`, so the value will be tracked on `watchEffect`. + * + * It recommend use `mutate` for non-primitive values, all levels of the object will be reactive. + * + * @param mutator + */ + mutate(mutator: Mutator): void; +} + +export interface ReadonlySignal { + (): Readonly; +} + +export function useSignal(initialValue: T): Signal { + const state = shallowRef(initialValue); + + return createSignal(state); +} + +export function useComputed(getter: ComputedGetter, debugOptions?: DebuggerOptions): ReadonlySignal; +export function useComputed(options: WritableComputedOptions, debugOptions?: DebuggerOptions): Signal; +export function useComputed( + getterOrOptions: ComputedGetter | WritableComputedOptions, + debugOptions?: DebuggerOptions +) { + const isGetter = typeof getterOrOptions === 'function'; + + const computedValue = computed(getterOrOptions as any, debugOptions); + + if (isGetter) { + return () => computedValue.value as ReadonlySignal; + } + + return createSignal(computedValue); +} + +function createSignal(state: ShallowRef | WritableComputedRef): Signal { + const signal = () => state.value; + + signal.set = (value: T) => { + state.value = value; + }; + + signal.update = (updater: Updater) => { + state.value = updater(state.value); + }; + + signal.mutate = (mutator: Mutator) => { + mutator(state.value); + triggerRef(state); + }; + + return signal; +} diff --git a/packages/hooks/src/use-state.ts b/packages/hooks/src/use-state.ts deleted file mode 100644 index 41f1b333..00000000 --- a/packages/hooks/src/use-state.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { ref } from 'vue'; -import type { Ref } from 'vue'; - -/** - * useRef - * - * it is a simple ref management hook wrapped by vue3's ref function. - * - * to resolve the ref type problem about `UnwrapRef` - * - * @param initValue - */ -export function useRef(initValue: T) { - const refValue = ref(initValue) as Ref; - - return refValue; -} - -/** - * useState - * - * define a state and a setState function - * - * @param initValue - */ -export function useState(initValue: T) { - const state = useRef(initValue); - - function setState(value: T) { - state.value = value; - } - - return [state, setState] as const; -} - -interface Signal { - (): T; - /** - * the ref object of the signal, but it is readonly - * - * equal to `const ref = ref(initValue);` - */ - readonly ref: Readonly>; - /** - * set the value of the signal - * - * @param value - */ - set(value: T): void; - /** - * update the value of the signal - * - * @param fn update function - */ - update(fn: (value: T) => T): void; -} - -/** - * useSignal - * - * @param initValue - */ -export function useSignal(initValue: T) { - const [state, setState] = useState(initValue); - - function updateState(fn: (value: T) => T) { - const updatedValue = fn(state.value); - setState(updatedValue); - } - - const signal = function signal() { - return state.value; - } as Signal; - - (signal as any).ref = state; - - signal.set = setState; - signal.update = updateState; - - return signal; -}