diff --git a/src/react/useObserve.ts b/src/react/useObserve.ts index efdb74ef..3ce610bd 100644 --- a/src/react/useObserve.ts +++ b/src/react/useObserve.ts @@ -10,17 +10,22 @@ import { } from '@legendapp/state'; import { useRef } from 'react'; import { useUnmountOnce } from './useUnmount'; +import { useObservable } from './useObservable'; -export function useObserve(run: (e: ObserveEvent) => T | void, options?: ObserveOptions): () => void; +export interface UseObserveOptions extends ObserveOptions { + deps?: any[]; +} + +export function useObserve(run: (e: ObserveEvent) => T | void, options?: UseObserveOptions): () => void; export function useObserve( selector: Selector, reaction?: (e: ObserveEventCallback) => any, - options?: ObserveOptions, + options?: UseObserveOptions, ): () => void; export function useObserve( selector: Selector | ((e: ObserveEvent) => any), - reactionOrOptions?: ((e: ObserveEventCallback) => any) | ObserveOptions, - options?: ObserveOptions, + reactionOrOptions?: ((e: ObserveEventCallback) => any) | UseObserveOptions, + options?: UseObserveOptions, ): () => void { let reaction: ((e: ObserveEventCallback) => any) | undefined; if (isFunction(reactionOrOptions)) { @@ -29,13 +34,27 @@ export function useObserve( options = reactionOrOptions; } + const deps = options?.deps; + + // Create a deps observable to be watched by the created observable + const depsObs$ = deps ? useObservable(deps) : undefined; + // Update depsObs with the deps array + if (depsObs$) { + depsObs$.set(deps! as any[]); + } + const ref = useRef<{ selector?: Selector | ((e: ObserveEvent) => T | void) | ObservableParam; reaction?: (e: ObserveEventCallback) => any; dispose?: () => void; }>({}); - ref.current.selector = selector; + ref.current.selector = deps + ? () => { + depsObs$?.get(); + return computeSelector(selector); + } + : selector; ref.current.reaction = reaction; if (!ref.current.dispose) { diff --git a/src/react/useObserveEffect.ts b/src/react/useObserveEffect.ts index e6ada731..1a7474c2 100644 --- a/src/react/useObserveEffect.ts +++ b/src/react/useObserveEffect.ts @@ -1,25 +1,19 @@ -import { - ObserveOptions, - isFunction, - ObservableParam, - observe, - ObserveEvent, - ObserveEventCallback, - Selector, -} from '@legendapp/state'; +import { ObservableParam, ObserveEvent, ObserveEventCallback, Selector, isFunction, observe } from '@legendapp/state'; import { useRef } from 'react'; import { useMountOnce } from './useMount'; +import { useObservable } from './useObservable'; +import type { UseObserveOptions } from './useObserve'; -export function useObserveEffect(run: (e: ObserveEvent) => T | void, options?: ObserveOptions): void; +export function useObserveEffect(run: (e: ObserveEvent) => T | void, options?: UseObserveOptions): void; export function useObserveEffect( selector: Selector, reaction?: (e: ObserveEventCallback) => any, - options?: ObserveOptions, + options?: UseObserveOptions, ): void; export function useObserveEffect( selector: Selector | ((e: ObserveEvent) => any), - reactionOrOptions?: ((e: ObserveEventCallback) => any) | ObserveOptions, - options?: ObserveOptions, + reactionOrOptions?: ((e: ObserveEventCallback) => any) | UseObserveOptions, + options?: UseObserveOptions, ): void { let reaction: ((e: ObserveEventCallback) => any) | undefined; if (isFunction(reactionOrOptions)) { @@ -28,6 +22,15 @@ export function useObserveEffect( options = reactionOrOptions; } + const deps = options?.deps; + + // Create a deps observable to be watched by the created observable + const depsObs$ = deps ? useObservable(deps) : undefined; + // Update depsObs with the deps array + if (depsObs$) { + depsObs$.set(deps! as any[]); + } + const ref = useRef<{ selector: Selector | ((e: ObserveEvent) => T | void) | ObservableParam; reaction?: (e: ObserveEventCallback) => any; @@ -38,6 +41,7 @@ export function useObserveEffect( observe( ((e: ObserveEventCallback) => { const { selector } = ref.current as { selector: (e: ObserveEvent) => T | void }; + depsObs$?.get(); return isFunction(selector) ? selector(e) : selector; }) as any, (e) => ref.current.reaction?.(e), diff --git a/tests/persist.test.ts b/tests/persist.test.ts index 4912ee60..8efac615 100644 --- a/tests/persist.test.ts +++ b/tests/persist.test.ts @@ -1214,7 +1214,7 @@ describe('isSyncEnabled', () => { const obs$ = observable>(); const ev$ = event(); let gets = 0; - let sets = 0; + const sets$ = observable(0); const state$ = syncObservable(obs$, { get: () => { gets++; @@ -1227,7 +1227,7 @@ describe('isSyncEnabled', () => { }; }, set: () => { - sets++; + sets$.set((v) => v + 1); }, } as SyncedOptions); @@ -1238,20 +1238,20 @@ describe('isSyncEnabled', () => { }, }); expect(gets).toEqual(1); - expect(sets).toEqual(0); + expect(sets$.get()).toEqual(0); obs$.id1.test.set('hello'); - await promiseTimeout(0); + await when(() => sets$.get() === 1); expect(gets).toEqual(1); - expect(sets).toEqual(1); + expect(sets$.get()).toEqual(1); ev$.fire(); obs$.get(); expect(gets).toEqual(2); - expect(sets).toEqual(1); + expect(sets$.get()).toEqual(1); state$.isSyncEnabled.set(false); @@ -1260,12 +1260,12 @@ describe('isSyncEnabled', () => { await promiseTimeout(0); expect(gets).toEqual(2); - expect(sets).toEqual(1); + expect(sets$.get()).toEqual(1); ev$.fire(); expect(gets).toEqual(2); - expect(sets).toEqual(1); + expect(sets$.get()).toEqual(1); }); }); describe('synced is observer', () => { diff --git a/tests/react.test.tsx b/tests/react.test.tsx index 2aea2ef6..0c3b62bc 100644 --- a/tests/react.test.tsx +++ b/tests/react.test.tsx @@ -999,6 +999,61 @@ describe('useObserve', () => { expect(num).toEqual(1); expect(numSets).toEqual(0); }); + test('useObserve with a deps array', () => { + let num = 0; + let numInner = 0; + const obsOuter$ = observable(0); + const obsInner$: Observable = observable(0); + let lastObserved: number | undefined = undefined; + const Test = observer(function Test() { + const dep = obsOuter$.get(); + useObserve( + () => { + numInner++; + lastObserved = obsInner$.get(); + }, + { deps: [dep] }, + ); + num++; + + return createElement('div', undefined); + }); + function App() { + return createElement(Test); + } + render(createElement(App)); + + expect(num).toEqual(1); + expect(numInner).toEqual(1); + expect(lastObserved).toEqual(0); + + // If deps array changes it should refresh observable + act(() => { + obsOuter$.set(1); + }); + + expect(num).toEqual(2); + expect(numInner).toEqual(2); + expect(lastObserved).toEqual(0); + + // If inner dep changes it should run again without rendering + act(() => { + obsInner$.set(1); + }); + + expect(num).toEqual(2); + expect(numInner).toEqual(3); + expect(lastObserved).toEqual(1); + + // If deps array changes it should refresh observable + act(() => { + obsOuter$.set(2); + }); + + expect(num).toEqual(3); + expect(numInner).toEqual(4); + expect(lastObserved).toEqual(1); + }); }); describe('useObserveEffect', () => { @@ -1040,6 +1095,61 @@ describe('useObserveEffect', () => { state$.set((v) => v + 1); expect(num).toEqual(3); }); + test('useObserve with a deps array', () => { + let num = 0; + let numInner = 0; + const obsOuter$ = observable(0); + const obsInner$: Observable = observable(0); + let lastObserved: number | undefined = undefined; + const Test = observer(function Test() { + const dep = obsOuter$.get(); + useObserveEffect( + () => { + numInner++; + lastObserved = obsInner$.get(); + }, + { deps: [dep] }, + ); + num++; + + return createElement('div', undefined); + }); + function App() { + return createElement(Test); + } + render(createElement(App)); + + expect(num).toEqual(1); + expect(numInner).toEqual(1); + expect(lastObserved).toEqual(0); + + // If deps array changes it should refresh observable + act(() => { + obsOuter$.set(1); + }); + + expect(num).toEqual(2); + expect(numInner).toEqual(2); + expect(lastObserved).toEqual(0); + + // If inner dep changes it should run again without rendering + act(() => { + obsInner$.set(1); + }); + + expect(num).toEqual(2); + expect(numInner).toEqual(3); + expect(lastObserved).toEqual(1); + + // If deps array changes it should refresh observable + act(() => { + obsOuter$.set(2); + }); + + expect(num).toEqual(3); + expect(numInner).toEqual(4); + expect(lastObserved).toEqual(1); + }); }); describe('observer', () => {