Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added watchEffect function in Stock #163

Merged
merged 6 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fresh-owls-explain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'stocked': minor
---

Create watchEffect function, deprecate watch & watchAll
26 changes: 22 additions & 4 deletions src/hooks/useObservers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useRef } from 'react';
import { MutableRefObject, useCallback, useRef } from 'react';
import { createPxth, deepGet, isInnerPxth, Pxth, samePxth } from 'pxth';
import invariant from 'tiny-invariant';

Expand All @@ -8,10 +8,18 @@ import { ObserverArray, ObserverKey } from '../utils/ObserverArray';
import { useLazyRef } from '../utils/useLazyRef';

export type ObserversControl<T> = {
/** Watch stock value. Returns cleanup function. */
/**
* Watch stock value. Returns cleanup function.
* @deprecated - use watchEffect instead
*/
watch: <V>(path: Pxth<V>, observer: Observer<V>) => () => void;
/** Watch all stock values. Returns cleanup function. */
/**
* Watch all stock values. Returns cleanup function.
* @deprecated - use watchEffect instead
*/
watchAll: (observer: Observer<T>) => () => void;
/** Watch stock value. Returns cleanup function. Calls observer instantly. */
watchEffect: <V>(path: Pxth<V>, observer: Observer<V>) => () => void;
/** Check if value is observed or not. */
isObserved: <V>(path: Pxth<V>) => boolean;
/** Notify all observers, which are children of specified path */
Expand All @@ -23,7 +31,7 @@ export type ObserversControl<T> = {
};

/** Hook, wraps functionality of observers storage (add, remove, notify tree of observers, etc.) */
export const useObservers = <T>(): ObserversControl<T> => {
export const useObservers = <T>(values: MutableRefObject<T>): ObserversControl<T> => {
const observersMap = useRef<PxthMap<ObserverArray<unknown>>>(new PxthMap());
const batchUpdateObservers = useLazyRef<ObserverArray<BatchUpdate<T>>>(() => new ObserverArray());

Expand Down Expand Up @@ -74,6 +82,15 @@ export const useObservers = <T>(): ObserversControl<T> => {

const watchAll = useCallback((observer: Observer<T>) => watch(createPxth<T>([]), observer), [watch]);

const watchEffect = useCallback(
<V>(path: Pxth<V>, observer: Observer<V>) => {
observer(deepGet(values.current, path));
const key = observe(path, observer);
return () => stopObserving(path, key);
},
[observe, stopObserving, values],
);

const watchBatchUpdates = useCallback(
(observer: Observer<BatchUpdate<T>>) => {
const key = observeBatchUpdates(observer);
Expand Down Expand Up @@ -123,6 +140,7 @@ export const useObservers = <T>(): ObserversControl<T> => {
return {
watch,
watchAll,
watchEffect,
watchBatchUpdates,
isObserved,
notifySubTree,
Expand Down
7 changes: 5 additions & 2 deletions src/hooks/useStock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ export type StockConfig<T extends object> = {
*/
export const useStock = <T extends object>({ initialValues, debugName }: StockConfig<T>): Stock<T> => {
const values = useLazyRef<T>(() => cloneDeep(initialValues));
const { notifySubTree, notifyAll, watch, watchAll, watchBatchUpdates, isObserved } = useObservers<T>();
const { notifySubTree, notifyAll, watch, watchAll, watchEffect, watchBatchUpdates, isObserved } =
useObservers<T>(values);

const setValue = useCallback(
<V>(path: Pxth<V>, action: SetStateAction<V>) => {
Expand Down Expand Up @@ -78,6 +79,7 @@ export const useStock = <T extends object>({ initialValues, debugName }: StockCo
resetValues,
watch,
watchAll,
watchEffect,
watchBatchUpdates,
isObserved,
debugName,
Expand All @@ -86,14 +88,15 @@ export const useStock = <T extends object>({ initialValues, debugName }: StockCo
[
getValue,
getValues,
debugName,
setValue,
setValues,
resetValues,
watch,
watchAll,
watchEffect,
watchBatchUpdates,
isObserved,
debugName,
],
);

Expand Down
6 changes: 3 additions & 3 deletions src/hooks/useStockValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,19 @@ export const useStockValue = <V, T extends object = object>(
): V => {
const stock = useStockContext(customStock, proxy);

const { watch, getValue } = stock;
const { watchEffect, getValue } = stock;

const [, forceUpdate] = useReducer((val) => val + 1, 0);

const value = useLazyRef<V>(() => getValue(path));

useEffect(
() =>
watch(path, (newValue) => {
watchEffect(path, (newValue) => {
value.current = newValue;
forceUpdate();
}),
[path, watch, value],
[path, watchEffect, value],
);

return value.current;
Expand Down
9 changes: 9 additions & 0 deletions src/typings/MappingProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ export class MappingProxy<T> extends StockProxy<T> {
return defaultWatch(normalPath, (value) => observer(this.mapValue(value, path, normalPath) as V));
};

public watchEffect = <V>(
path: Pxth<V>,
observer: Observer<V>,
defaultWatchEffect: <U>(path: Pxth<U>, observer: Observer<U>) => () => void,
) => {
const normalPath = this.getNormalPath(path);
return defaultWatchEffect(normalPath, (value) => observer(this.mapValue(value, path, normalPath) as V));
};

public getValue = <V>(path: Pxth<V>, defaultGetValue: <U>(path: Pxth<U>) => U): V => {
const normalPath = this.getNormalPath(path);
return this.mapValue(defaultGetValue(normalPath), path, normalPath) as V;
Expand Down
12 changes: 11 additions & 1 deletion src/typings/StockProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,23 @@ export abstract class StockProxy<T> {
defaultGetValue: <U>(path: Pxth<U>) => U,
) => void;

/** Function for watching proxied value. Should return cleanup. */
/**
* Function for watching proxied value. Should return cleanup.
* @deprecated
*/
public abstract watch: <V>(
path: Pxth<V>,
observer: Observer<V>,
defaultWatch: <U>(path: Pxth<U>, observer: Observer<U>) => () => void,
) => () => void;

/** Function for watching proxied value. Should return cleanup. Calls observer instantly. */
public abstract watchEffect: <V>(
path: Pxth<V>,
observer: Observer<V>,
defaultWatchEffect: <U>(path: Pxth<U>, observer: Observer<U>) => () => void,
) => () => void;

/** Function to access proxied value. */
public abstract getValue: <V>(path: Pxth<V>, defaultGetValue: <U>(path: Pxth<U>) => U) => V;

Expand Down
15 changes: 14 additions & 1 deletion src/utils/useInterceptors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const intercept = <T extends (...args: any[]) => any>(

/** Intercepts stock's `observe`, `stopObserving` and `setValue` functions, if proxy is provided. */
export const useInterceptors = <T extends object>(stock: Stock<T>, proxy?: StockProxy<unknown>): Stock<T> => {
const { watch, setValue, getValue, setValues, getValues } = stock;
const { watch, watchEffect, setValue, getValue, setValues, getValues } = stock;

useEffect(
() =>
Expand All @@ -57,6 +57,18 @@ export const useInterceptors = <T extends object>(stock: Stock<T>, proxy?: Stock
[watch, proxy],
);

const interceptedWatchEffect = useCallback(
<V>(path: Pxth<V>, observer: Observer<V>) =>
intercept(
proxy,
path as Pxth<unknown>,
watchEffect,
(path: Pxth<V>, observer: Observer<V>) => proxy!.watchEffect<V>(path, observer, watchEffect),
[path, observer],
),
[watchEffect, proxy],
);

const interceptedSetValue = useCallback(
<V>(path: Pxth<V>, value: SetStateAction<V>) =>
intercept(
Expand Down Expand Up @@ -114,6 +126,7 @@ export const useInterceptors = <T extends object>(stock: Stock<T>, proxy?: Stock
return {
...stock,
watch: interceptedWatch,
watchEffect: interceptedWatchEffect,
setValue: interceptedSetValue,
getValue: interceptedGetValue,
getValues: interceptedGetValues,
Expand Down
1 change: 1 addition & 0 deletions test/DummyProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ export class DummyProxy extends StockProxy<unknown> {
public getNormalPath = <V>(path: Pxth<V>) => path;
public setValue = () => {};
public watch = () => () => {};
public watchEffect = () => () => {};
public getValue = <V>(path: Pxth<V>, defaultGetValue: <U>(path: Pxth<U>) => U) => defaultGetValue(path) as V;
}
21 changes: 20 additions & 1 deletion test/hooks/useObservers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { createPxth, getPxthSegments } from 'pxth';

import { useObservers } from '../../src';

const renderUseObserversHook = () => renderHook(() => useObservers());
const renderUseObserversHook = (values: object = {}) =>
renderHook(() =>
useObservers({
current: values,
}),
);

describe('Observer tests', () => {
it('should call value observer', () => {
Expand All @@ -21,6 +26,20 @@ describe('Observer tests', () => {
expect(observer).toBeCalled();
});

it('should call observer instantly with "watchEffect"', () => {
const { result } = renderUseObserversHook({
b: 'hello',
});

const observer = jest.fn();

act(() => {
result.current.watchEffect(createPxth(['b']), observer);
});

expect(observer).toBeCalled();
});

it('Should call all values observer', async () => {
const { result } = renderUseObserversHook();

Expand Down
85 changes: 85 additions & 0 deletions test/typings/MappingProxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,27 @@ describe('Mapping proxy', () => {
expect(getPxthSegments(defaultObserve.mock.calls[0][0])).toStrictEqual(['a', 'b', 'd']);
});

it('observe/stopObserving value - with "watchEffect"', () => {
const proxy = new MappingProxy(
{ hello: createPxth(['a', 'b', 'c']), bye: createPxth(['a', 'b', 'd']) },
createPxth(['asdf']),
);

const defaultObserve = jest.fn();
const observer = jest.fn();

defaultObserve.mockReturnValue(0);

proxy.watchEffect(createPxth(['asdf', 'hello']), observer, defaultObserve);

expect(getPxthSegments(defaultObserve.mock.calls[0][0])).toStrictEqual(['a', 'b', 'c']);

defaultObserve.mockClear();

proxy.watchEffect(createPxth(['asdf', 'bye']), observer, defaultObserve);
expect(getPxthSegments(defaultObserve.mock.calls[0][0])).toStrictEqual(['a', 'b', 'd']);
});

it('observe/stopObserving (empty mapping path)', () => {
const proxy = new MappingProxy(createPxth(['a', 'd', 'c']), createPxth(['asdf']));

Expand Down Expand Up @@ -150,6 +171,70 @@ describe('Mapping proxy', () => {
expect(observer).toBeCalledWith(fullUser.personalData);
});

it('calling observer fns - with watchEffect', () => {
const fullUser = {
personalData: {
name: {
firstName: 'Hello',
lastName: 'World',
},
birthday: new Date('2020.12.26'),
},
registrationDate: new Date('2020.12.31'),
notify: true,
};
const rawData = {
registeredUser: {
name: fullUser.personalData.name.firstName,
surname: fullUser.personalData.name.lastName,
dates: {
registration: fullUser.registrationDate,
},
},
dateOfBirth: fullUser.personalData.birthday,
};

const proxy = new MappingProxy<RegisteredUser>(getUserMapSource(), createPxth(['registeredUser']));

const observers: Observer<unknown>[] = [];

const defaultObserve = jest.fn((path, observer) => {
observer(deepGet(rawData, path));
observers.push(observer);
return () => observers.splice(observers.indexOf(observer), 1);
});
const observer = jest.fn();

proxy.watchEffect(
createPxth(['registeredUser', 'personalData', 'name', 'firstName']),
observer,
defaultObserve,
);

expect(observer).toBeCalledWith(fullUser.personalData.name.firstName);
observer.mockClear();

expect(getPxthSegments(defaultObserve.mock.calls[0][0])).toStrictEqual(['registeredUser', 'name']);

defaultObserve.mockClear();

proxy.watchEffect(createPxth(['registeredUser', 'personalData', 'name']), observer, defaultObserve);

expect(observer).toBeCalledWith(fullUser.personalData.name);
observer.mockClear();

expect(getPxthSegments(defaultObserve.mock.calls[0][0])).toStrictEqual(['registeredUser']);

defaultObserve.mockClear();

proxy.watchEffect(createPxth(['registeredUser', 'personalData']), observer, defaultObserve);

expect(observer).toBeCalledWith(fullUser.personalData);
observer.mockClear();

expect(getPxthSegments(defaultObserve.mock.calls[0][0])).toStrictEqual([]);
});

it('calling observer fns (complex cases)', () => {
const fullData = {
truck: {
Expand Down
36 changes: 36 additions & 0 deletions test/utils/useInterceptors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ describe('hit cases', () => {
expect(observer).toBeCalledTimes(1);
expect(observer).toBeCalledWith('asdf');
});
it('no proxy - with "watchEffect"', () => {
const [{ result }] = renderUseInterceptorsHook(defaultInitialValues);

const observer = jest.fn();
act(() => {
const cleanup = result.current.watchEffect(createPxth(['dest', 'bye']), observer);
cleanup();
});

expect(observer).toBeCalledTimes(1);
expect(observer).toBeCalledWith('asdf');
});
it('non activated proxy', () => {
expect(() => renderUseInterceptorsHook(defaultInitialValues, new DummyProxy(createPxth(['asdf'])))).toThrow();
});
Expand Down Expand Up @@ -111,6 +123,30 @@ describe('proxy', () => {
expect(getValue).toBeCalledTimes(1);
});

it('should call proxy functions - with "watchEffect"', () => {
const proxy = new DummyProxy(createPxth(['dest']));

const watchEffect: jest.Mock<any, any> = jest.fn(() => () => {});

proxy.watchEffect = watchEffect;
proxy.activate();
const [{ result }] = renderUseInterceptorsHook(defaultInitialValues, proxy);

const observer = jest.fn();

act(() => {
const cleanup = result.current.watchEffect(createPxth(['dest']), observer);
const cleanup2 = result.current.watchEffect(createPxth(['asdf']), observer);
cleanup();
cleanup2();
});

expect(getPxthSegments(watchEffect.mock.calls[0][0])).toStrictEqual(['dest']);
expect(watchEffect).toBeCalledWith(expect.anything(), observer, expect.any(Function));
expect(watchEffect).toBeCalledTimes(1);
expect(observer).toBeCalled();
});

it('should handle setValues / getValues properly', async () => {
const proxy = new DummyProxy(createPxth(['dest']));

Expand Down
Loading