react-rx 3.1.2 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +8 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +9 -9
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
- package/src/__tests__/strictmode.test.ts +28 -4
- package/src/__tests__/useObservable.test-d.ts +1 -1
- package/src/__tests__/useObservable.test.ts +12 -5
- package/src/useObservable.ts +6 -9
- package/src/useObservableEvent.ts +4 -4
package/dist/index.cjs
CHANGED
|
@@ -27,14 +27,14 @@ function useObservable(observable, initialValue) {
|
|
|
27
27
|
}),
|
|
28
28
|
// Ensure that the cache entry is deleted when the observable completes or errors.
|
|
29
29
|
rxjs.finalize(() => cache.delete(observable)),
|
|
30
|
-
rxjs.share()
|
|
31
|
-
), entry.
|
|
30
|
+
rxjs.share({ resetOnRefCountZero: () => rxjs.timer(0, rxjs.asapScheduler) })
|
|
31
|
+
), entry.observable.subscribe().unsubscribe(), cache.set(observable, entry);
|
|
32
32
|
}
|
|
33
33
|
const instance = cache.get(observable);
|
|
34
|
-
return
|
|
34
|
+
return {
|
|
35
35
|
subscribe: (onStoreChange) => {
|
|
36
36
|
const subscription = instance.observable.subscribe(onStoreChange);
|
|
37
|
-
return
|
|
37
|
+
return () => {
|
|
38
38
|
subscription.unsubscribe();
|
|
39
39
|
};
|
|
40
40
|
},
|
|
@@ -51,12 +51,12 @@ function useObservable(observable, initialValue) {
|
|
|
51
51
|
typeof initialValueRef.current > "u" ? void 0 : () => initialValueRef.current
|
|
52
52
|
);
|
|
53
53
|
}
|
|
54
|
-
function useObservableEvent(
|
|
55
|
-
const [[calls$, call]] = react.useState(() => observableCallback.observableCallback()),
|
|
54
|
+
function useObservableEvent(handleEvent) {
|
|
55
|
+
const [[calls$, call]] = react.useState(() => observableCallback.observableCallback()), onEvent = useEffectEvent.useEffectEvent((observable) => handleEvent(observable));
|
|
56
56
|
return react.useEffect(() => {
|
|
57
|
-
const subscription = calls$.pipe(
|
|
57
|
+
const subscription = calls$.pipe((observable) => onEvent(observable)).subscribe();
|
|
58
58
|
return () => subscription.unsubscribe();
|
|
59
|
-
}, [
|
|
59
|
+
}, [calls$, onEvent]), call;
|
|
60
60
|
}
|
|
61
61
|
exports.useObservable = useObservable;
|
|
62
62
|
exports.useObservableEvent = useObservableEvent;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":["../src/useObservable.ts","../src/useObservableEvent.ts"],"sourcesContent":["import {useEffect, useMemo, useRef, useSyncExternalStore} from 'react'\nimport {\n catchError,\n finalize,\n type Observable,\n type ObservedValueOf,\n of,\n share,\n
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/useObservable.ts","../src/useObservableEvent.ts"],"sourcesContent":["import {useEffect, useMemo, useRef, useSyncExternalStore} from 'react'\nimport {\n asapScheduler,\n catchError,\n finalize,\n type Observable,\n type ObservedValueOf,\n of,\n share,\n timer,\n} from 'rxjs'\nimport {map, tap} from 'rxjs/operators'\n\nfunction getValue<T>(value: T): T extends () => infer U ? U : T {\n return typeof value === 'function' ? value() : value\n}\n\ninterface CacheRecord<T> {\n observable: Observable<void>\n snapshot: T\n error?: unknown\n}\n\nconst cache = new WeakMap<Observable<any>, CacheRecord<any>>()\n\n/** @public */\nexport function useObservable<ObservableType extends Observable<any>>(\n observable: ObservableType,\n initialValue: ObservedValueOf<ObservableType> | (() => ObservedValueOf<ObservableType>),\n): ObservedValueOf<ObservableType>\n/** @public */\nexport function useObservable<ObservableType extends Observable<any>>(\n observable: ObservableType,\n): undefined | ObservedValueOf<ObservableType>\n/** @public */\nexport function useObservable<ObservableType extends Observable<any>, InitialValue>(\n observable: ObservableType,\n initialValue: InitialValue | (() => InitialValue),\n): InitialValue | ObservedValueOf<ObservableType>\n/** @public */\nexport function useObservable<ObservableType extends Observable<any>, InitialValue>(\n observable: ObservableType,\n initialValue?: InitialValue | (() => InitialValue),\n): InitialValue | ObservedValueOf<ObservableType> {\n /**\n * Store the initialValue in a ref, as we don't want a changed `initialValue` to trigger a re-subscription.\n * But we also don't want the initialValue to be stale if the observable changes.\n */\n const initialValueRef = useRef(getValue(initialValue) as ObservedValueOf<ObservableType>)\n\n /**\n * Ensures that the initialValue is always up-to-date in case the observable changes.\n */\n useEffect(() => {\n initialValueRef.current = getValue(initialValue) as ObservedValueOf<ObservableType>\n }, [initialValue])\n\n const store = useMemo(() => {\n if (!cache.has(observable)) {\n const entry: Partial<CacheRecord<ObservedValueOf<ObservableType>>> = {\n snapshot: initialValueRef.current,\n }\n entry.observable = observable.pipe(\n map((value) => ({snapshot: value, error: undefined})),\n catchError((error) => of({snapshot: undefined, error})),\n tap(({snapshot, error}) => {\n entry.snapshot = snapshot\n entry.error = error\n }),\n // Note: any value or error emitted by the provided observable will be mapped to the cache entry's mutable state\n // and the observable is thereafter only used as a notifier to call `onStoreChange`, hence the `void` return type.\n map((value) => void value),\n // Ensure that the cache entry is deleted when the observable completes or errors.\n finalize(() => cache.delete(observable)),\n share({resetOnRefCountZero: () => timer(0, asapScheduler)}),\n )\n\n // Eagerly subscribe to sync set `entry.currentValue` to what the observable returns, and keep the observable alive until the component unmounts.\n const subscription = entry.observable.subscribe()\n subscription.unsubscribe()\n\n cache.set(observable, entry as CacheRecord<ObservedValueOf<ObservableType>>)\n }\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const instance = cache.get(observable)!\n\n return {\n subscribe: (onStoreChange: () => void) => {\n const subscription = instance.observable.subscribe(onStoreChange)\n return () => {\n subscription.unsubscribe()\n }\n },\n getSnapshot: () => {\n if (instance.error) {\n throw instance.error\n }\n return instance.snapshot\n },\n }\n }, [observable])\n\n return useSyncExternalStore<ObservedValueOf<ObservableType>>(\n store.subscribe,\n store.getSnapshot,\n typeof initialValueRef.current === 'undefined' ? undefined : () => initialValueRef.current,\n )\n}\n","import {observableCallback} from 'observable-callback'\nimport {useEffect, useState} from 'react'\nimport {type Observable} from 'rxjs'\nimport {useEffectEvent} from 'use-effect-event'\n\n/** @public */\nexport function useObservableEvent<T, U>(\n handleEvent: (arg: Observable<T>) => Observable<U>,\n): (arg: T) => void {\n const [[calls$, call]] = useState(() => observableCallback<T>())\n\n const onEvent = useEffectEvent((observable: Observable<T>) => handleEvent(observable))\n\n useEffect(() => {\n const subscription = calls$.pipe((observable) => onEvent(observable)).subscribe()\n return () => subscription.unsubscribe()\n }, [calls$, onEvent])\n\n return call\n}\n"],"names":["useRef","useEffect","useMemo","map","catchError","of","tap","finalize","share","timer","asapScheduler","useSyncExternalStore","useState","observableCallback","useEffectEvent"],"mappings":";;;AAaA,SAAS,SAAY,OAA2C;AAC9D,SAAO,OAAO,SAAU,aAAa,MAAA,IAAU;AACjD;AAQA,MAAM,4BAAY;AAiBF,SAAA,cACd,YACA,cACgD;AAKhD,QAAM,kBAAkBA,MAAA,OAAO,SAAS,YAAY,CAAoC;AAKxFC,QAAAA,UAAU,MAAM;AACE,oBAAA,UAAU,SAAS,YAAY;AAAA,EAAA,GAC9C,CAAC,YAAY,CAAC;AAEX,QAAA,QAAQC,MAAAA,QAAQ,MAAM;AAC1B,QAAI,CAAC,MAAM,IAAI,UAAU,GAAG;AAC1B,YAAM,QAA+D;AAAA,QACnE,UAAU,gBAAgB;AAAA,MAAA;AAE5B,YAAM,aAAa,WAAW;AAAA,QAC5BC,cAAI,CAAC,WAAW,EAAC,UAAU,OAAO,OAAO,SAAW;AAAA,QACpDC,gBAAW,CAAC,UAAUC,QAAG,EAAC,UAAU,QAAW,MAAK,CAAC,CAAC;AAAA,QACtDC,UAAAA,IAAI,CAAC,EAAC,UAAU,YAAW;AACnB,gBAAA,WAAW,UACjB,MAAM,QAAQ;AAAA,QAAA,CACf;AAAA;AAAA;AAAA,QAGDH,UAAA,IAAI,CAAC,UAAO;AAAA,QAAA,CAAa;AAAA;AAAA,QAEzBI,KAAAA,SAAS,MAAM,MAAM,OAAO,UAAU,CAAC;AAAA,QACvCC,KAAA,MAAM,EAAC,qBAAqB,MAAMC,WAAM,GAAGC,KAAa,aAAA,GAAE;AAAA,MAAA,GAIvC,MAAM,WAAW,YACzB,eAEb,MAAM,IAAI,YAAY,KAAqD;AAAA,IAC7E;AAEM,UAAA,WAAW,MAAM,IAAI,UAAU;AAE9B,WAAA;AAAA,MACL,WAAW,CAAC,kBAA8B;AACxC,cAAM,eAAe,SAAS,WAAW,UAAU,aAAa;AAChE,eAAO,MAAM;AACX,uBAAa,YAAY;AAAA,QAAA;AAAA,MAE7B;AAAA,MACA,aAAa,MAAM;AACjB,YAAI,SAAS;AACX,gBAAM,SAAS;AAEjB,eAAO,SAAS;AAAA,MAClB;AAAA,IAAA;AAAA,EACF,GACC,CAAC,UAAU,CAAC;AAER,SAAAC,MAAA;AAAA,IACL,MAAM;AAAA,IACN,MAAM;AAAA,IACN,OAAO,gBAAgB,UAAY,MAAc,SAAY,MAAM,gBAAgB;AAAA,EAAA;AAEvF;ACrGO,SAAS,mBACd,aACkB;AAClB,QAAM,CAAC,CAAC,QAAQ,IAAI,CAAC,IAAIC,MAAAA,SAAS,MAAMC,mBAAsB,mBAAA,CAAC,GAEzD,UAAUC,eAAA,eAAe,CAAC,eAA8B,YAAY,UAAU,CAAC;AAErF,SAAAb,MAAA,UAAU,MAAM;AACR,UAAA,eAAe,OAAO,KAAK,CAAC,eAAe,QAAQ,UAAU,CAAC,EAAE;AAC/D,WAAA,MAAM,aAAa;EACzB,GAAA,CAAC,QAAQ,OAAO,CAAC,GAEb;AACT;;;"}
|
package/dist/index.d.cts
CHANGED
|
@@ -15,12 +15,12 @@ export declare function useObservable<ObservableType extends Observable<any>>(
|
|
|
15
15
|
/** @public */
|
|
16
16
|
export declare function useObservable<ObservableType extends Observable<any>, InitialValue>(
|
|
17
17
|
observable: ObservableType,
|
|
18
|
-
initialValue: InitialValue,
|
|
18
|
+
initialValue: InitialValue | (() => InitialValue),
|
|
19
19
|
): InitialValue | ObservedValueOf<ObservableType>
|
|
20
20
|
|
|
21
21
|
/** @public */
|
|
22
22
|
export declare function useObservableEvent<T, U>(
|
|
23
|
-
|
|
23
|
+
handleEvent: (arg: Observable<T>) => Observable<U>,
|
|
24
24
|
): (arg: T) => void
|
|
25
25
|
|
|
26
26
|
export {}
|
package/dist/index.d.ts
CHANGED
|
@@ -15,12 +15,12 @@ export declare function useObservable<ObservableType extends Observable<any>>(
|
|
|
15
15
|
/** @public */
|
|
16
16
|
export declare function useObservable<ObservableType extends Observable<any>, InitialValue>(
|
|
17
17
|
observable: ObservableType,
|
|
18
|
-
initialValue: InitialValue,
|
|
18
|
+
initialValue: InitialValue | (() => InitialValue),
|
|
19
19
|
): InitialValue | ObservedValueOf<ObservableType>
|
|
20
20
|
|
|
21
21
|
/** @public */
|
|
22
22
|
export declare function useObservableEvent<T, U>(
|
|
23
|
-
|
|
23
|
+
handleEvent: (arg: Observable<T>) => Observable<U>,
|
|
24
24
|
): (arg: T) => void
|
|
25
25
|
|
|
26
26
|
export {}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useRef, useEffect, useMemo, useSyncExternalStore, useState } from "react";
|
|
2
|
-
import { catchError, of, finalize, share } from "rxjs";
|
|
2
|
+
import { catchError, of, finalize, share, timer, asapScheduler } from "rxjs";
|
|
3
3
|
import { map, tap } from "rxjs/operators";
|
|
4
4
|
import { observableCallback } from "observable-callback";
|
|
5
5
|
import { useEffectEvent } from "use-effect-event";
|
|
@@ -29,14 +29,14 @@ function useObservable(observable, initialValue) {
|
|
|
29
29
|
}),
|
|
30
30
|
// Ensure that the cache entry is deleted when the observable completes or errors.
|
|
31
31
|
finalize(() => cache.delete(observable)),
|
|
32
|
-
share()
|
|
33
|
-
), entry.
|
|
32
|
+
share({ resetOnRefCountZero: () => timer(0, asapScheduler) })
|
|
33
|
+
), entry.observable.subscribe().unsubscribe(), cache.set(observable, entry);
|
|
34
34
|
}
|
|
35
35
|
const instance = cache.get(observable);
|
|
36
|
-
return
|
|
36
|
+
return {
|
|
37
37
|
subscribe: (onStoreChange) => {
|
|
38
38
|
const subscription = instance.observable.subscribe(onStoreChange);
|
|
39
|
-
return
|
|
39
|
+
return () => {
|
|
40
40
|
subscription.unsubscribe();
|
|
41
41
|
};
|
|
42
42
|
},
|
|
@@ -53,12 +53,12 @@ function useObservable(observable, initialValue) {
|
|
|
53
53
|
typeof initialValueRef.current > "u" ? void 0 : () => initialValueRef.current
|
|
54
54
|
);
|
|
55
55
|
}
|
|
56
|
-
function useObservableEvent(
|
|
57
|
-
const [[calls$, call]] = useState(() => observableCallback()),
|
|
56
|
+
function useObservableEvent(handleEvent) {
|
|
57
|
+
const [[calls$, call]] = useState(() => observableCallback()), onEvent = useEffectEvent((observable) => handleEvent(observable));
|
|
58
58
|
return useEffect(() => {
|
|
59
|
-
const subscription = calls$.pipe(
|
|
59
|
+
const subscription = calls$.pipe((observable) => onEvent(observable)).subscribe();
|
|
60
60
|
return () => subscription.unsubscribe();
|
|
61
|
-
}, [
|
|
61
|
+
}, [calls$, onEvent]), call;
|
|
62
62
|
}
|
|
63
63
|
export {
|
|
64
64
|
useObservable,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/useObservable.ts","../src/useObservableEvent.ts"],"sourcesContent":["import {useEffect, useMemo, useRef, useSyncExternalStore} from 'react'\nimport {\n catchError,\n finalize,\n type Observable,\n type ObservedValueOf,\n of,\n share,\n
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/useObservable.ts","../src/useObservableEvent.ts"],"sourcesContent":["import {useEffect, useMemo, useRef, useSyncExternalStore} from 'react'\nimport {\n asapScheduler,\n catchError,\n finalize,\n type Observable,\n type ObservedValueOf,\n of,\n share,\n timer,\n} from 'rxjs'\nimport {map, tap} from 'rxjs/operators'\n\nfunction getValue<T>(value: T): T extends () => infer U ? U : T {\n return typeof value === 'function' ? value() : value\n}\n\ninterface CacheRecord<T> {\n observable: Observable<void>\n snapshot: T\n error?: unknown\n}\n\nconst cache = new WeakMap<Observable<any>, CacheRecord<any>>()\n\n/** @public */\nexport function useObservable<ObservableType extends Observable<any>>(\n observable: ObservableType,\n initialValue: ObservedValueOf<ObservableType> | (() => ObservedValueOf<ObservableType>),\n): ObservedValueOf<ObservableType>\n/** @public */\nexport function useObservable<ObservableType extends Observable<any>>(\n observable: ObservableType,\n): undefined | ObservedValueOf<ObservableType>\n/** @public */\nexport function useObservable<ObservableType extends Observable<any>, InitialValue>(\n observable: ObservableType,\n initialValue: InitialValue | (() => InitialValue),\n): InitialValue | ObservedValueOf<ObservableType>\n/** @public */\nexport function useObservable<ObservableType extends Observable<any>, InitialValue>(\n observable: ObservableType,\n initialValue?: InitialValue | (() => InitialValue),\n): InitialValue | ObservedValueOf<ObservableType> {\n /**\n * Store the initialValue in a ref, as we don't want a changed `initialValue` to trigger a re-subscription.\n * But we also don't want the initialValue to be stale if the observable changes.\n */\n const initialValueRef = useRef(getValue(initialValue) as ObservedValueOf<ObservableType>)\n\n /**\n * Ensures that the initialValue is always up-to-date in case the observable changes.\n */\n useEffect(() => {\n initialValueRef.current = getValue(initialValue) as ObservedValueOf<ObservableType>\n }, [initialValue])\n\n const store = useMemo(() => {\n if (!cache.has(observable)) {\n const entry: Partial<CacheRecord<ObservedValueOf<ObservableType>>> = {\n snapshot: initialValueRef.current,\n }\n entry.observable = observable.pipe(\n map((value) => ({snapshot: value, error: undefined})),\n catchError((error) => of({snapshot: undefined, error})),\n tap(({snapshot, error}) => {\n entry.snapshot = snapshot\n entry.error = error\n }),\n // Note: any value or error emitted by the provided observable will be mapped to the cache entry's mutable state\n // and the observable is thereafter only used as a notifier to call `onStoreChange`, hence the `void` return type.\n map((value) => void value),\n // Ensure that the cache entry is deleted when the observable completes or errors.\n finalize(() => cache.delete(observable)),\n share({resetOnRefCountZero: () => timer(0, asapScheduler)}),\n )\n\n // Eagerly subscribe to sync set `entry.currentValue` to what the observable returns, and keep the observable alive until the component unmounts.\n const subscription = entry.observable.subscribe()\n subscription.unsubscribe()\n\n cache.set(observable, entry as CacheRecord<ObservedValueOf<ObservableType>>)\n }\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const instance = cache.get(observable)!\n\n return {\n subscribe: (onStoreChange: () => void) => {\n const subscription = instance.observable.subscribe(onStoreChange)\n return () => {\n subscription.unsubscribe()\n }\n },\n getSnapshot: () => {\n if (instance.error) {\n throw instance.error\n }\n return instance.snapshot\n },\n }\n }, [observable])\n\n return useSyncExternalStore<ObservedValueOf<ObservableType>>(\n store.subscribe,\n store.getSnapshot,\n typeof initialValueRef.current === 'undefined' ? undefined : () => initialValueRef.current,\n )\n}\n","import {observableCallback} from 'observable-callback'\nimport {useEffect, useState} from 'react'\nimport {type Observable} from 'rxjs'\nimport {useEffectEvent} from 'use-effect-event'\n\n/** @public */\nexport function useObservableEvent<T, U>(\n handleEvent: (arg: Observable<T>) => Observable<U>,\n): (arg: T) => void {\n const [[calls$, call]] = useState(() => observableCallback<T>())\n\n const onEvent = useEffectEvent((observable: Observable<T>) => handleEvent(observable))\n\n useEffect(() => {\n const subscription = calls$.pipe((observable) => onEvent(observable)).subscribe()\n return () => subscription.unsubscribe()\n }, [calls$, onEvent])\n\n return call\n}\n"],"names":[],"mappings":";;;;;AAaA,SAAS,SAAY,OAA2C;AAC9D,SAAO,OAAO,SAAU,aAAa,MAAA,IAAU;AACjD;AAQA,MAAM,4BAAY;AAiBF,SAAA,cACd,YACA,cACgD;AAKhD,QAAM,kBAAkB,OAAO,SAAS,YAAY,CAAoC;AAKxF,YAAU,MAAM;AACE,oBAAA,UAAU,SAAS,YAAY;AAAA,EAAA,GAC9C,CAAC,YAAY,CAAC;AAEX,QAAA,QAAQ,QAAQ,MAAM;AAC1B,QAAI,CAAC,MAAM,IAAI,UAAU,GAAG;AAC1B,YAAM,QAA+D;AAAA,QACnE,UAAU,gBAAgB;AAAA,MAAA;AAE5B,YAAM,aAAa,WAAW;AAAA,QAC5B,IAAI,CAAC,WAAW,EAAC,UAAU,OAAO,OAAO,SAAW;AAAA,QACpD,WAAW,CAAC,UAAU,GAAG,EAAC,UAAU,QAAW,MAAK,CAAC,CAAC;AAAA,QACtD,IAAI,CAAC,EAAC,UAAU,YAAW;AACnB,gBAAA,WAAW,UACjB,MAAM,QAAQ;AAAA,QAAA,CACf;AAAA;AAAA;AAAA,QAGD,IAAI,CAAC,UAAO;AAAA,QAAA,CAAa;AAAA;AAAA,QAEzB,SAAS,MAAM,MAAM,OAAO,UAAU,CAAC;AAAA,QACvC,MAAM,EAAC,qBAAqB,MAAM,MAAM,GAAG,aAAa,GAAE;AAAA,MAAA,GAIvC,MAAM,WAAW,YACzB,eAEb,MAAM,IAAI,YAAY,KAAqD;AAAA,IAC7E;AAEM,UAAA,WAAW,MAAM,IAAI,UAAU;AAE9B,WAAA;AAAA,MACL,WAAW,CAAC,kBAA8B;AACxC,cAAM,eAAe,SAAS,WAAW,UAAU,aAAa;AAChE,eAAO,MAAM;AACX,uBAAa,YAAY;AAAA,QAAA;AAAA,MAE7B;AAAA,MACA,aAAa,MAAM;AACjB,YAAI,SAAS;AACX,gBAAM,SAAS;AAEjB,eAAO,SAAS;AAAA,MAClB;AAAA,IAAA;AAAA,EACF,GACC,CAAC,UAAU,CAAC;AAER,SAAA;AAAA,IACL,MAAM;AAAA,IACN,MAAM;AAAA,IACN,OAAO,gBAAgB,UAAY,MAAc,SAAY,MAAM,gBAAgB;AAAA,EAAA;AAEvF;ACrGO,SAAS,mBACd,aACkB;AAClB,QAAM,CAAC,CAAC,QAAQ,IAAI,CAAC,IAAI,SAAS,MAAM,mBAAsB,CAAC,GAEzD,UAAU,eAAe,CAAC,eAA8B,YAAY,UAAU,CAAC;AAErF,SAAA,UAAU,MAAM;AACR,UAAA,eAAe,OAAO,KAAK,CAAC,eAAe,QAAQ,UAAU,CAAC,EAAE;AAC/D,WAAA,MAAM,aAAa;EACzB,GAAA,CAAC,QAAQ,OAAO,CAAC,GAEb;AACT;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-rx",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "React + RxJS = <3",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"action",
|
|
@@ -78,32 +78,32 @@
|
|
|
78
78
|
"use-effect-event": "^1.0.2"
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
|
-
"@sanity/pkg-utils": "^6.10.
|
|
81
|
+
"@sanity/pkg-utils": "^6.10.4",
|
|
82
82
|
"@sanity/prettier-config": "^1.0.2",
|
|
83
83
|
"@sanity/semantic-release-preset": "^5.0.0",
|
|
84
|
-
"@testing-library/dom": "^10.
|
|
84
|
+
"@testing-library/dom": "^10.4.0",
|
|
85
85
|
"@testing-library/react": "^16.0.0",
|
|
86
86
|
"@types/node": "^18.17.5",
|
|
87
87
|
"@types/react": "^18.3.3",
|
|
88
88
|
"@types/react-dom": "^18.3.0",
|
|
89
|
-
"@typescript-eslint/eslint-plugin": "^7.
|
|
90
|
-
"@typescript-eslint/parser": "^7.
|
|
89
|
+
"@typescript-eslint/eslint-plugin": "^7.17.0",
|
|
90
|
+
"@typescript-eslint/parser": "^7.17.0",
|
|
91
91
|
"eslint": "^8.57.0",
|
|
92
92
|
"eslint-config-prettier": "^9.1.0",
|
|
93
|
-
"eslint-plugin-prettier": "^5.1
|
|
94
|
-
"eslint-plugin-react": "^7.
|
|
95
|
-
"eslint-plugin-react-compiler": "0.0.0-experimental-
|
|
93
|
+
"eslint-plugin-prettier": "^5.2.1",
|
|
94
|
+
"eslint-plugin-react": "^7.35.0",
|
|
95
|
+
"eslint-plugin-react-compiler": "0.0.0-experimental-9ed098e-20240725",
|
|
96
96
|
"eslint-plugin-react-hooks": "^4.6.2",
|
|
97
97
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
98
98
|
"jsdom": "^24.1.0",
|
|
99
|
-
"prettier": "^3.3.
|
|
99
|
+
"prettier": "^3.3.3",
|
|
100
100
|
"react": "^18.3.1",
|
|
101
101
|
"react-dom": "^18.3.1",
|
|
102
102
|
"react-test-renderer": "^18.3.1",
|
|
103
103
|
"rxjs": "^7.8.1",
|
|
104
104
|
"semantic-release": "^24.0.0",
|
|
105
105
|
"typescript": "5.5.3",
|
|
106
|
-
"vitest": "^2.0.
|
|
106
|
+
"vitest": "^2.0.4"
|
|
107
107
|
},
|
|
108
108
|
"peerDependencies": {
|
|
109
109
|
"react": "^18.3 || >=19.0.0-rc",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {act, render} from '@testing-library/react'
|
|
2
|
-
import {createElement, Fragment, StrictMode, useEffect} from 'react'
|
|
2
|
+
import {createElement, Fragment, StrictMode, useEffect, useMemo} from 'react'
|
|
3
3
|
import {BehaviorSubject, Observable} from 'rxjs'
|
|
4
4
|
import {expect, test} from 'vitest'
|
|
5
5
|
|
|
@@ -39,7 +39,7 @@ test('Strict mode should trigger double mount effects and re-renders', async ()
|
|
|
39
39
|
expect(mountCount).toEqual(2)
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
-
test('Strict mode should unsubscribe the source observable on unmount', () => {
|
|
42
|
+
test('Strict mode should unsubscribe the source observable on unmount', async () => {
|
|
43
43
|
const subscribed: number[] = []
|
|
44
44
|
const unsubscribed: number[] = []
|
|
45
45
|
let nextId = 0
|
|
@@ -57,7 +57,31 @@ test('Strict mode should unsubscribe the source observable on unmount', () => {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
const {rerender} = render(createElement(StrictMode, null, createElement(ObservableComponent)))
|
|
60
|
-
expect(subscribed).toEqual([0
|
|
60
|
+
expect(subscribed).toEqual([0])
|
|
61
61
|
rerender(createElement(StrictMode, null, createElement('div')))
|
|
62
|
-
|
|
62
|
+
await Promise.resolve()
|
|
63
|
+
expect(unsubscribed).toEqual([0])
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('Strict mode should unsubscribe the source observable on unmount if its created in a useMemo', async () => {
|
|
67
|
+
let subscriberCount: number = 0
|
|
68
|
+
const getObservable = () =>
|
|
69
|
+
new Observable(() => {
|
|
70
|
+
subscriberCount++
|
|
71
|
+
return () => {
|
|
72
|
+
subscriberCount--
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
function ObservableComponent() {
|
|
77
|
+
const memoObservable = useMemo(() => getObservable(), [])
|
|
78
|
+
useObservable(memoObservable)
|
|
79
|
+
return createElement(Fragment, null)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const {rerender} = render(createElement(StrictMode, null, createElement(ObservableComponent)))
|
|
83
|
+
expect(subscriberCount, 'Subscriber count should be 2').toBe(2)
|
|
84
|
+
rerender(createElement(StrictMode, null, createElement('div')))
|
|
85
|
+
await Promise.resolve()
|
|
86
|
+
expect(subscriberCount, 'Subscriber count should be 0').toBe(0)
|
|
63
87
|
})
|
|
@@ -22,6 +22,6 @@ test('useObservable with initial value if a different type returns a union of th
|
|
|
22
22
|
const observable = of('foo')
|
|
23
23
|
|
|
24
24
|
expectTypeOf(useObservable(observable, 1)).toEqualTypeOf<string | number>()
|
|
25
|
-
|
|
25
|
+
expectTypeOf(useObservable(observable, () => 1)).toEqualTypeOf<string | number>()
|
|
26
26
|
expectTypeOf(useObservable(observable, 'foo')).toEqualTypeOf<string>()
|
|
27
27
|
})
|
|
@@ -6,7 +6,7 @@ import {expect, test} from 'vitest'
|
|
|
6
6
|
|
|
7
7
|
import {useObservable} from '../useObservable'
|
|
8
8
|
|
|
9
|
-
test('should subscribe immediately on component mount and unsubscribe on component unmount', () => {
|
|
9
|
+
test('should subscribe immediately on component mount and unsubscribe on component unmount', async () => {
|
|
10
10
|
let subscribed = false
|
|
11
11
|
const observable = new Observable(() => {
|
|
12
12
|
subscribed = true
|
|
@@ -21,10 +21,11 @@ test('should subscribe immediately on component mount and unsubscribe on compone
|
|
|
21
21
|
expect(subscribed).toBe(true)
|
|
22
22
|
|
|
23
23
|
unmount()
|
|
24
|
+
await Promise.resolve()
|
|
24
25
|
expect(subscribed).toBe(false)
|
|
25
26
|
})
|
|
26
27
|
|
|
27
|
-
test('should only subscribe once when given same observable on re-renders', () => {
|
|
28
|
+
test('should only subscribe once when given same observable on re-renders', async () => {
|
|
28
29
|
let subscriptionCount = 0
|
|
29
30
|
const observable = new Observable(() => {
|
|
30
31
|
subscriptionCount++
|
|
@@ -37,6 +38,7 @@ test('should only subscribe once when given same observable on re-renders', () =
|
|
|
37
38
|
rerender()
|
|
38
39
|
expect(subscriptionCount).toBe(1)
|
|
39
40
|
unmount()
|
|
41
|
+
await Promise.resolve()
|
|
40
42
|
|
|
41
43
|
renderHook(() => useObservable(observable))
|
|
42
44
|
expect(subscriptionCount).toBe(2)
|
|
@@ -103,7 +105,7 @@ test('should have passed initialValue as initial value from delayed observables'
|
|
|
103
105
|
unmount()
|
|
104
106
|
})
|
|
105
107
|
|
|
106
|
-
test('should rerender with initial value if component unmounts and then remounts', () => {
|
|
108
|
+
test('should rerender with initial value if component unmounts and then remounts', async () => {
|
|
107
109
|
const values$ = new Subject<string>()
|
|
108
110
|
const firstHook = renderHook(() => useObservable(values$, 'initial'))
|
|
109
111
|
|
|
@@ -113,13 +115,14 @@ test('should rerender with initial value if component unmounts and then remounts
|
|
|
113
115
|
expect(firstHook.result.current).toBe('something')
|
|
114
116
|
|
|
115
117
|
firstHook.unmount()
|
|
118
|
+
await Promise.resolve()
|
|
116
119
|
|
|
117
120
|
const nextHook = renderHook(() => useObservable(values$, 'initial2'))
|
|
118
121
|
|
|
119
122
|
expect(nextHook.result.current).toBe('initial2')
|
|
120
123
|
})
|
|
121
124
|
|
|
122
|
-
test('should share the observable between each concurrent subscribing hook', () => {
|
|
125
|
+
test('should share the observable between each concurrent subscribing hook', async () => {
|
|
123
126
|
let subscribeCount = 0
|
|
124
127
|
const observable = new Observable<number>((subscriber) => {
|
|
125
128
|
subscriber.next(subscribeCount++)
|
|
@@ -130,13 +133,14 @@ test('should share the observable between each concurrent subscribing hook', ()
|
|
|
130
133
|
expect(secondHook.result.current).toBe(0)
|
|
131
134
|
firstHook.unmount()
|
|
132
135
|
secondHook.unmount()
|
|
136
|
+
await Promise.resolve()
|
|
133
137
|
|
|
134
138
|
const thirdHook = renderHook(() => useObservable(observable))
|
|
135
139
|
expect(thirdHook.result.current).toBe(1)
|
|
136
140
|
thirdHook.unmount()
|
|
137
141
|
})
|
|
138
142
|
|
|
139
|
-
test('should restart any completed observable on mount', () => {
|
|
143
|
+
test('should restart any completed observable on mount', async () => {
|
|
140
144
|
let subscribeCount = 0
|
|
141
145
|
let unsubscribeCount = 0
|
|
142
146
|
|
|
@@ -178,12 +182,15 @@ test('should restart any completed observable on mount', () => {
|
|
|
178
182
|
expect(unsubscribeCount).toBe(1)
|
|
179
183
|
|
|
180
184
|
firstHook.unmount()
|
|
185
|
+
await Promise.resolve()
|
|
181
186
|
|
|
182
187
|
const secondHook = renderHook(() => useObservable(observable))
|
|
183
188
|
expect(secondHook.result.current).toBe(undefined)
|
|
184
189
|
expect(subscribeCount).toBe(2)
|
|
185
190
|
expect(unsubscribeCount).toBe(1)
|
|
186
191
|
secondHook.unmount()
|
|
192
|
+
await Promise.resolve()
|
|
193
|
+
|
|
187
194
|
expect(unsubscribeCount).toBe(2)
|
|
188
195
|
})
|
|
189
196
|
|
package/src/useObservable.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import {useEffect, useMemo, useRef, useSyncExternalStore} from 'react'
|
|
2
2
|
import {
|
|
3
|
+
asapScheduler,
|
|
3
4
|
catchError,
|
|
4
5
|
finalize,
|
|
5
6
|
type Observable,
|
|
6
7
|
type ObservedValueOf,
|
|
7
8
|
of,
|
|
8
9
|
share,
|
|
9
|
-
|
|
10
|
+
timer,
|
|
10
11
|
} from 'rxjs'
|
|
11
12
|
import {map, tap} from 'rxjs/operators'
|
|
12
13
|
|
|
@@ -15,7 +16,6 @@ function getValue<T>(value: T): T extends () => infer U ? U : T {
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
interface CacheRecord<T> {
|
|
18
|
-
subscription: Subscription
|
|
19
19
|
observable: Observable<void>
|
|
20
20
|
snapshot: T
|
|
21
21
|
error?: unknown
|
|
@@ -35,7 +35,7 @@ export function useObservable<ObservableType extends Observable<any>>(
|
|
|
35
35
|
/** @public */
|
|
36
36
|
export function useObservable<ObservableType extends Observable<any>, InitialValue>(
|
|
37
37
|
observable: ObservableType,
|
|
38
|
-
initialValue: InitialValue,
|
|
38
|
+
initialValue: InitialValue | (() => InitialValue),
|
|
39
39
|
): InitialValue | ObservedValueOf<ObservableType>
|
|
40
40
|
/** @public */
|
|
41
41
|
export function useObservable<ObservableType extends Observable<any>, InitialValue>(
|
|
@@ -72,24 +72,21 @@ export function useObservable<ObservableType extends Observable<any>, InitialVal
|
|
|
72
72
|
map((value) => void value),
|
|
73
73
|
// Ensure that the cache entry is deleted when the observable completes or errors.
|
|
74
74
|
finalize(() => cache.delete(observable)),
|
|
75
|
-
share(),
|
|
75
|
+
share({resetOnRefCountZero: () => timer(0, asapScheduler)}),
|
|
76
76
|
)
|
|
77
77
|
|
|
78
78
|
// Eagerly subscribe to sync set `entry.currentValue` to what the observable returns, and keep the observable alive until the component unmounts.
|
|
79
|
-
|
|
79
|
+
const subscription = entry.observable.subscribe()
|
|
80
|
+
subscription.unsubscribe()
|
|
80
81
|
|
|
81
82
|
cache.set(observable, entry as CacheRecord<ObservedValueOf<ObservableType>>)
|
|
82
83
|
}
|
|
83
84
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
84
85
|
const instance = cache.get(observable)!
|
|
85
|
-
if (instance.subscription.closed) {
|
|
86
|
-
instance.subscription = instance.observable.subscribe()
|
|
87
|
-
}
|
|
88
86
|
|
|
89
87
|
return {
|
|
90
88
|
subscribe: (onStoreChange: () => void) => {
|
|
91
89
|
const subscription = instance.observable.subscribe(onStoreChange)
|
|
92
|
-
instance.subscription.unsubscribe()
|
|
93
90
|
return () => {
|
|
94
91
|
subscription.unsubscribe()
|
|
95
92
|
}
|
|
@@ -5,16 +5,16 @@ import {useEffectEvent} from 'use-effect-event'
|
|
|
5
5
|
|
|
6
6
|
/** @public */
|
|
7
7
|
export function useObservableEvent<T, U>(
|
|
8
|
-
|
|
8
|
+
handleEvent: (arg: Observable<T>) => Observable<U>,
|
|
9
9
|
): (arg: T) => void {
|
|
10
10
|
const [[calls$, call]] = useState(() => observableCallback<T>())
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const onEvent = useEffectEvent((observable: Observable<T>) => handleEvent(observable))
|
|
13
13
|
|
|
14
14
|
useEffect(() => {
|
|
15
|
-
const subscription = calls$.pipe(
|
|
15
|
+
const subscription = calls$.pipe((observable) => onEvent(observable)).subscribe()
|
|
16
16
|
return () => subscription.unsubscribe()
|
|
17
|
-
}, [
|
|
17
|
+
}, [calls$, onEvent])
|
|
18
18
|
|
|
19
19
|
return call
|
|
20
20
|
}
|