Skip to content

Commit

Permalink
Add deps options to useObserve and useObserveEffect
Browse files Browse the repository at this point in the history
  • Loading branch information
jmeistrich committed Jul 1, 2024
1 parent 2ff99ae commit c9a6c02
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 26 deletions.
29 changes: 24 additions & 5 deletions src/react/useObserve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,22 @@ import {
} from '@legendapp/state';
import { useRef } from 'react';
import { useUnmountOnce } from './useUnmount';
import { useObservable } from './useObservable';

export function useObserve<T>(run: (e: ObserveEvent<T>) => T | void, options?: ObserveOptions): () => void;
export interface UseObserveOptions extends ObserveOptions {
deps?: any[];
}

export function useObserve<T>(run: (e: ObserveEvent<T>) => T | void, options?: UseObserveOptions): () => void;
export function useObserve<T>(
selector: Selector<T>,
reaction?: (e: ObserveEventCallback<T>) => any,
options?: ObserveOptions,
options?: UseObserveOptions,
): () => void;
export function useObserve<T>(
selector: Selector<T> | ((e: ObserveEvent<T>) => any),
reactionOrOptions?: ((e: ObserveEventCallback<T>) => any) | ObserveOptions,
options?: ObserveOptions,
reactionOrOptions?: ((e: ObserveEventCallback<T>) => any) | UseObserveOptions,
options?: UseObserveOptions,
): () => void {
let reaction: ((e: ObserveEventCallback<T>) => any) | undefined;
if (isFunction(reactionOrOptions)) {
Expand All @@ -29,13 +34,27 @@ export function useObserve<T>(
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<T> | ((e: ObserveEvent<T>) => T | void) | ObservableParam<T>;
reaction?: (e: ObserveEventCallback<T>) => 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) {
Expand Down
30 changes: 17 additions & 13 deletions src/react/useObserveEffect.ts
Original file line number Diff line number Diff line change
@@ -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<T>(run: (e: ObserveEvent<T>) => T | void, options?: ObserveOptions): void;
export function useObserveEffect<T>(run: (e: ObserveEvent<T>) => T | void, options?: UseObserveOptions): void;
export function useObserveEffect<T>(
selector: Selector<T>,
reaction?: (e: ObserveEventCallback<T>) => any,
options?: ObserveOptions,
options?: UseObserveOptions,
): void;
export function useObserveEffect<T>(
selector: Selector<T> | ((e: ObserveEvent<T>) => any),
reactionOrOptions?: ((e: ObserveEventCallback<T>) => any) | ObserveOptions,
options?: ObserveOptions,
reactionOrOptions?: ((e: ObserveEventCallback<T>) => any) | UseObserveOptions,
options?: UseObserveOptions,
): void {
let reaction: ((e: ObserveEventCallback<T>) => any) | undefined;
if (isFunction(reactionOrOptions)) {
Expand All @@ -28,6 +22,15 @@ export function useObserveEffect<T>(
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<T> | ((e: ObserveEvent<T>) => T | void) | ObservableParam<T>;
reaction?: (e: ObserveEventCallback<T>) => any;
Expand All @@ -38,6 +41,7 @@ export function useObserveEffect<T>(
observe<T>(
((e: ObserveEventCallback<T>) => {
const { selector } = ref.current as { selector: (e: ObserveEvent<T>) => T | void };
depsObs$?.get();
return isFunction(selector) ? selector(e) : selector;
}) as any,
(e) => ref.current.reaction?.(e),
Expand Down
16 changes: 8 additions & 8 deletions tests/persist.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1214,7 +1214,7 @@ describe('isSyncEnabled', () => {
const obs$ = observable<Record<string, { id: string; test: string }>>();
const ev$ = event();
let gets = 0;
let sets = 0;
const sets$ = observable(0);
const state$ = syncObservable(obs$, {
get: () => {
gets++;
Expand All @@ -1227,7 +1227,7 @@ describe('isSyncEnabled', () => {
};
},
set: () => {
sets++;
sets$.set((v) => v + 1);
},
} as SyncedOptions);

Expand All @@ -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);

Expand All @@ -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', () => {
Expand Down
110 changes: 110 additions & 0 deletions tests/react.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down

0 comments on commit c9a6c02

Please sign in to comment.