diff --git a/.changeset/polite-ligers-travel.md b/.changeset/polite-ligers-travel.md
new file mode 100644
index 0000000..a43e6f4
--- /dev/null
+++ b/.changeset/polite-ligers-travel.md
@@ -0,0 +1,5 @@
+---
+'@unisonjs/vue': patch
+---
+
+Fix trigger of hook ref
diff --git a/packages/vue/__tests__/toUnisonHook.spec.tsx b/packages/vue/__tests__/toUnisonHook.spec.tsx
new file mode 100644
index 0000000..74bd0b9
--- /dev/null
+++ b/packages/vue/__tests__/toUnisonHook.spec.tsx
@@ -0,0 +1,215 @@
+import { render } from '@testing-library/react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import { $unison, watchEffect, toUnisonHook, nextTick, ref } from '../src/index';
+
+type Action = 'body' | 'effect (pre)' | 'effect (post)' | 'hook' | 'render';
+
+describe('toUnisonHook test suite', () => {
+ it("should track react hook's result object", async () => {
+ function useBooleanValue() {
+ const [state, setState] = useState(false);
+
+ const mutate = useCallback(() => {
+ setState((value) => !value);
+ }, []);
+
+ return { value: state, mutate };
+ }
+
+ const booleanValue = toUnisonHook(useBooleanValue);
+
+ let dummy;
+ const events: Action[] = [];
+ const Comp = $unison(() => {
+ const { value, mutate } = booleanValue();
+
+ watchEffect(() => {
+ dummy = value.value;
+ });
+ return () => {
+ events.push('render');
+ return (
+
+ );
+ };
+ });
+
+ const { container } = render();
+
+ const counter = container.querySelector('#action') as HTMLButtonElement;
+
+ expect(dummy).toBe(false);
+
+ counter.click();
+
+ await nextTick();
+ expect(dummy).toBe(true);
+ expect(events).toEqual(['render', 'render']);
+ });
+
+ it('should update once', async () => {
+ function useBooleanValue() {
+ const [state, setState] = useState(false);
+
+ const mutate = useCallback(() => {
+ setState((value) => !value);
+ }, []);
+
+ return {};
+ }
+
+ const booleanValue = toUnisonHook(useBooleanValue, { shallow: true });
+
+ const events: Action[] = [];
+ const Comp = $unison(() => {
+ booleanValue();
+ const counter = ref(0);
+
+ return () => {
+ events.push('render');
+ return (
+
+ );
+ };
+ });
+
+ const { container } = render();
+
+ const counter = container.querySelector('#action') as HTMLButtonElement;
+
+ counter.click();
+ await nextTick();
+ expect(events).toEqual(['render', 'render']);
+ });
+ it("should track react hook's result (shallow)", async () => {
+ function useValue() {
+ const [state, setState] = useState({ value: 1 });
+
+ useEffect(() => {
+ const timeout = setTimeout(() => void setState({ value: 2 }), 30);
+
+ return () => clearTimeout(timeout);
+ }, []);
+
+ return state;
+ }
+
+ const value = toUnisonHook(useValue);
+
+ let dummy;
+ const events: Action[] = [];
+ const VueComponent = $unison(() => {
+ const result = value();
+
+ watchEffect(() => {
+ dummy = result.value;
+ });
+
+ return () => {
+ events.push('render');
+ return ;
+ };
+ });
+
+ render();
+
+ expect(dummy.value).toBe(1);
+
+ await new Promise((res) => setTimeout(res, 30));
+
+ expect(dummy.value).toBe(2);
+ expect(events).toEqual(['render', 'render']);
+ });
+
+ it("should not track react hook's result deeply (shallow)", async () => {
+ function useBooleanValue() {
+ const [state, setState] = useState(false);
+
+ const mutate = useCallback(() => {
+ setState((value) => !value);
+ }, []);
+
+ return useMemo(() => ({ value: state, mutate }), []);
+ }
+
+ const booleanValue = toUnisonHook(useBooleanValue, { shallow: true });
+
+ let dummy;
+ const events: Action[] = [];
+ const VueComponent = $unison(() => {
+ const result = booleanValue();
+
+ watchEffect(() => {
+ events.push('effect (pre)');
+ dummy = result.value.value;
+ });
+
+ return () => {
+ events.push('render');
+ return (
+
+ );
+ };
+ });
+
+ const { container } = render();
+
+ const counter = container.querySelector('#action') as HTMLButtonElement;
+
+ expect(dummy).toBeDefined();
+
+ let prev = dummy;
+ counter.click();
+ await nextTick();
+
+ expect(dummy).toBe(prev);
+ expect(events).toEqual(['effect (pre)', 'render']);
+ });
+
+ it('should run hook when no effects is declared', async () => {
+ const events: Action[] = [];
+ function useBooleanValue() {
+ events.push('hook');
+ const [state, setState] = useState(false);
+
+ const mutate = useCallback(() => {
+ setState((value) => !value);
+ }, []);
+
+ return { value: state, mutate };
+ }
+
+ const booleanValue = toUnisonHook(useBooleanValue);
+
+ const Comp = $unison(() => {
+ const count = ref(0);
+ booleanValue();
+
+ function increment() {
+ count.value++;
+ }
+ return () => {
+ events.push('render');
+ return (
+
+ );
+ };
+ });
+
+ const { container } = render();
+
+ const counter = container.querySelector('#action') as HTMLButtonElement;
+
+ counter.click();
+ await nextTick();
+ expect(events).toEqual(['hook', 'render', 'hook', 'render']);
+ });
+});
diff --git a/packages/vue/src/react-hook/hookRef.ts b/packages/vue/src/react-hook/hookRef.ts
index a85d40b..9ccfb23 100644
--- a/packages/vue/src/react-hook/hookRef.ts
+++ b/packages/vue/src/react-hook/hookRef.ts
@@ -1,7 +1,7 @@
import { ReactiveFlags, TrackOpTypes } from '../reactivity/constants';
-import { type HookManager, currentListener, Dep, Listener } from '@unisonjs/core';
+import { type HookManager, BaseSignal, currentListener, Dep, Listener } from '@unisonjs/core';
-class HookRef {
+class HookRef extends BaseSignal {
/**
* @internal
*/
@@ -36,6 +36,7 @@ class HookRef {
public readonly [ReactiveFlags.IS_READONLY]: boolean = true;
constructor(manager: HookManager, hookIndex: number, valueIndex: number) {
+ super();
this.#manager = manager;
this.#hookIndex = hookIndex;
this.#valueIndex = valueIndex;
@@ -59,6 +60,15 @@ class HookRef {
return currentValue as T;
}
+
+ trigger() {
+ const {listeners} = this;
+ if (listeners) {
+ for (const listener of listeners) listener.trigger?.();
+ listeners.length = 0;
+ }
+ this.dep.trigger()
+ }
}
export default HookRef;