Skip to content

Commit

Permalink
Added watchEffect function in Stock
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexShukel committed Jan 22, 2024
1 parent 318e0b0 commit f44953a
Show file tree
Hide file tree
Showing 9 changed files with 96 additions and 13 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"typescript.tsdk": "node_modules\\typescript\\lib",
"editor.codeActionsOnSave": {
"source.fixAll": true
"source.fixAll": "explicit"
}
}
34 changes: 30 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,20 @@ 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 watchAllEffect instead
*/
watchAll: (observer: Observer<T>) => () => void;
/** Watch stock value. Returns cleanup function. Calls observer instantly. */
watchEffect: <V>(path: Pxth<V>, observer: Observer<V>) => () => void;
/** Watch all stock values. Returns cleanup function. Calls observer instantly. */
watchAllEffect: (observer: Observer<T>) => () => 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 +33,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 +84,20 @@ 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 watchAllEffect = useCallback(
(observer: Observer<T>) => watchEffect(createPxth<T>([]), observer),

Check warning on line 97 in src/hooks/useObservers.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 97 in src/hooks/useObservers.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function
[watchEffect],
);

const watchBatchUpdates = useCallback(
(observer: Observer<BatchUpdate<T>>) => {
const key = observeBatchUpdates(observer);
Expand Down Expand Up @@ -121,6 +145,8 @@ export const useObservers = <T>(): ObserversControl<T> => {
return {
watch,
watchAll,
watchEffect,
watchAllEffect,
watchBatchUpdates,
isObserved,
notifySubTree,
Expand Down
9 changes: 7 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, watchAllEffect, watchBatchUpdates, isObserved } =
useObservers<T>(values);

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

Check warning on line 70 in src/typings/MappingProxy.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function
path: Pxth<V>,
observer: Observer<V>,
defaultWatchEffect: <U>(path: Pxth<U>, observer: Observer<U>) => () => void,
) => {
const normalPath = this.getNormalPath(path);

Check warning on line 75 in src/typings/MappingProxy.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
return defaultWatchEffect(normalPath, (value) => observer(this.mapValue(value, path, normalPath) as V));

Check warning on line 76 in src/typings/MappingProxy.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 76 in src/typings/MappingProxy.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 76 in src/typings/MappingProxy.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function
};

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>) =>

Check warning on line 61 in src/utils/useInterceptors.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function
intercept(
proxy,
path as Pxth<unknown>,
watchEffect,
(path: Pxth<V>, observer: Observer<V>) => proxy!.watchEffect<V>(path, observer, watchEffect),

Check warning on line 66 in src/utils/useInterceptors.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 66 in src/utils/useInterceptors.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function
[path, observer],

Check warning on line 67 in src/utils/useInterceptors.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
),
[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

0 comments on commit f44953a

Please sign in to comment.