react-rx 3.1.2 → 4.0.0-canary.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 +26 -30
- 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 +28 -32
- package/dist/index.js.map +1 -1
- package/package.json +27 -29
- 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 +31 -45
- package/src/useObservableEvent.ts +4 -4
package/dist/index.cjs
CHANGED
|
@@ -6,35 +6,31 @@ function getValue(value) {
|
|
|
6
6
|
}
|
|
7
7
|
const cache = /* @__PURE__ */ new WeakMap();
|
|
8
8
|
function useObservable(observable, initialValue) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
if (!cache.has(observable)) {
|
|
10
|
+
const entry = {
|
|
11
|
+
snapshot: getValue(initialValue)
|
|
12
|
+
};
|
|
13
|
+
entry.observable = observable.pipe(
|
|
14
|
+
operators.map((value) => ({ snapshot: value, error: void 0 })),
|
|
15
|
+
rxjs.catchError((error) => rxjs.of({ snapshot: void 0, error })),
|
|
16
|
+
operators.tap(({ snapshot, error }) => {
|
|
17
|
+
entry.snapshot = snapshot, entry.error = error;
|
|
18
|
+
}),
|
|
19
|
+
// Note: any value or error emitted by the provided observable will be mapped to the cache entry's mutable state
|
|
20
|
+
// and the observable is thereafter only used as a notifier to call `onStoreChange`, hence the `void` return type.
|
|
21
|
+
operators.map((value) => {
|
|
22
|
+
}),
|
|
23
|
+
// Ensure that the cache entry is deleted when the observable completes or errors.
|
|
24
|
+
rxjs.finalize(() => cache.delete(observable)),
|
|
25
|
+
rxjs.share({ resetOnRefCountZero: () => rxjs.timer(0, rxjs.asapScheduler) })
|
|
26
|
+
), entry.observable.subscribe().unsubscribe(), cache.set(observable, entry);
|
|
27
|
+
}
|
|
13
28
|
const store = react.useMemo(() => {
|
|
14
|
-
if (!cache.has(observable)) {
|
|
15
|
-
const entry = {
|
|
16
|
-
snapshot: initialValueRef.current
|
|
17
|
-
};
|
|
18
|
-
entry.observable = observable.pipe(
|
|
19
|
-
operators.map((value) => ({ snapshot: value, error: void 0 })),
|
|
20
|
-
rxjs.catchError((error) => rxjs.of({ snapshot: void 0, error })),
|
|
21
|
-
operators.tap(({ snapshot, error }) => {
|
|
22
|
-
entry.snapshot = snapshot, entry.error = error;
|
|
23
|
-
}),
|
|
24
|
-
// Note: any value or error emitted by the provided observable will be mapped to the cache entry's mutable state
|
|
25
|
-
// and the observable is thereafter only used as a notifier to call `onStoreChange`, hence the `void` return type.
|
|
26
|
-
operators.map((value) => {
|
|
27
|
-
}),
|
|
28
|
-
// Ensure that the cache entry is deleted when the observable completes or errors.
|
|
29
|
-
rxjs.finalize(() => cache.delete(observable)),
|
|
30
|
-
rxjs.share()
|
|
31
|
-
), entry.subscription = entry.observable.subscribe(), cache.set(observable, entry);
|
|
32
|
-
}
|
|
33
29
|
const instance = cache.get(observable);
|
|
34
|
-
return
|
|
30
|
+
return {
|
|
35
31
|
subscribe: (onStoreChange) => {
|
|
36
32
|
const subscription = instance.observable.subscribe(onStoreChange);
|
|
37
|
-
return
|
|
33
|
+
return () => {
|
|
38
34
|
subscription.unsubscribe();
|
|
39
35
|
};
|
|
40
36
|
},
|
|
@@ -48,15 +44,15 @@ function useObservable(observable, initialValue) {
|
|
|
48
44
|
return react.useSyncExternalStore(
|
|
49
45
|
store.subscribe,
|
|
50
46
|
store.getSnapshot,
|
|
51
|
-
typeof
|
|
47
|
+
typeof initialValue > "u" ? void 0 : () => getValue(initialValue)
|
|
52
48
|
);
|
|
53
49
|
}
|
|
54
|
-
function useObservableEvent(
|
|
55
|
-
const [[calls$, call]] = react.useState(() => observableCallback.observableCallback()),
|
|
50
|
+
function useObservableEvent(handleEvent) {
|
|
51
|
+
const [[calls$, call]] = react.useState(() => observableCallback.observableCallback()), onEvent = useEffectEvent.useEffectEvent((observable) => handleEvent(observable));
|
|
56
52
|
return react.useEffect(() => {
|
|
57
|
-
const subscription = calls$.pipe(
|
|
53
|
+
const subscription = calls$.pipe((observable) => onEvent(observable)).subscribe();
|
|
58
54
|
return () => subscription.unsubscribe();
|
|
59
|
-
}, [
|
|
55
|
+
}, [calls$, onEvent]), call;
|
|
60
56
|
}
|
|
61
57
|
exports.useObservable = useObservable;
|
|
62
58
|
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 {
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/useObservable.ts","../src/useObservableEvent.ts"],"sourcesContent":["import {useMemo, 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 if (!cache.has(observable)) {\n const entry: Partial<CacheRecord<ObservedValueOf<ObservableType>>> = {\n snapshot: getValue(initialValue) as ObservedValueOf<ObservableType>,\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\n const store = useMemo(() => {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const instance = cache.get(observable)!\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 initialValue === 'undefined'\n ? undefined\n : () => getValue(initialValue) as ObservedValueOf<ObservableType>,\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":["map","catchError","of","tap","finalize","share","timer","asapScheduler","useMemo","useSyncExternalStore","useState","observableCallback","useEffectEvent","useEffect"],"mappings":";;;AAaA,SAAS,SAAY,OAA2C;AAC9D,SAAO,OAAO,SAAU,aAAa,MAAU,IAAA;AACjD;AAQA,MAAM,4BAAY,QAA2C;AAiB7C,SAAA,cACd,YACA,cACgD;AAChD,MAAI,CAAC,MAAM,IAAI,UAAU,GAAG;AAC1B,UAAM,QAA+D;AAAA,MACnE,UAAU,SAAS,YAAY;AAAA,IACjC;AACA,UAAM,aAAa,WAAW;AAAA,MAC5BA,cAAI,CAAC,WAAW,EAAC,UAAU,OAAO,OAAO,SAAW;AAAA,MACpDC,gBAAW,CAAC,UAAUC,QAAG,EAAC,UAAU,QAAW,MAAK,CAAC,CAAC;AAAA,MACtDC,UAAAA,IAAI,CAAC,EAAC,UAAU,YAAW;AACnB,cAAA,WAAW,UACjB,MAAM,QAAQ;AAAA,MAAA,CACf;AAAA;AAAA;AAAA,MAGDH,UAAA,IAAI,CAAC,UAAO;AAAA,MAAA,CAAa;AAAA;AAAA,MAEzBI,KAAAA,SAAS,MAAM,MAAM,OAAO,UAAU,CAAC;AAAA,MACvCC,KAAA,MAAM,EAAC,qBAAqB,MAAMC,WAAM,GAAGC,KAAa,aAAA,EAAE,CAAA;AAAA,IAAA,GAIvC,MAAM,WAAW,YACzB,YAEb,GAAA,MAAM,IAAI,YAAY,KAAqD;AAAA,EAAA;AAGvE,QAAA,QAAQC,MAAAA,QAAQ,MAAM;AAEpB,UAAA,WAAW,MAAM,IAAI,UAAU;AAC9B,WAAA;AAAA,MACL,WAAW,CAAC,kBAA8B;AACxC,cAAM,eAAe,SAAS,WAAW,UAAU,aAAa;AAChE,eAAO,MAAM;AACX,uBAAa,YAAY;AAAA,QAC3B;AAAA,MACF;AAAA,MACA,aAAa,MAAM;AACjB,YAAI,SAAS;AACX,gBAAM,SAAS;AAEjB,eAAO,SAAS;AAAA,MAAA;AAAA,IAEpB;AAAA,EAAA,GACC,CAAC,UAAU,CAAC;AAER,SAAAC,MAAA;AAAA,IACL,MAAM;AAAA,IACN,MAAM;AAAA,IACN,OAAO,eAAiB,MACpB,SACA,MAAM,SAAS,YAAY;AAAA,EACjC;AACF;AC1FO,SAAS,mBACd,aACkB;AAClB,QAAM,CAAC,CAAC,QAAQ,IAAI,CAAC,IAAIC,MAAS,SAAA,MAAMC,mBAAAA,mBAAuB,CAAA,GAEzD,UAAUC,eAAAA,eAAe,CAAC,eAA8B,YAAY,UAAU,CAAC;AAErF,SAAAC,MAAA,UAAU,MAAM;AACR,UAAA,eAAe,OAAO,KAAK,CAAC,eAAe,QAAQ,UAAU,CAAC,EAAE,UAAU;AACzE,WAAA,MAAM,aAAa,YAAY;AAAA,EACrC,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
|
-
import {
|
|
2
|
-
import { catchError, of, finalize, share } from "rxjs";
|
|
1
|
+
import { useMemo, useSyncExternalStore, useState, useEffect } from "react";
|
|
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";
|
|
@@ -8,35 +8,31 @@ function getValue(value) {
|
|
|
8
8
|
}
|
|
9
9
|
const cache = /* @__PURE__ */ new WeakMap();
|
|
10
10
|
function useObservable(observable, initialValue) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
if (!cache.has(observable)) {
|
|
12
|
+
const entry = {
|
|
13
|
+
snapshot: getValue(initialValue)
|
|
14
|
+
};
|
|
15
|
+
entry.observable = observable.pipe(
|
|
16
|
+
map((value) => ({ snapshot: value, error: void 0 })),
|
|
17
|
+
catchError((error) => of({ snapshot: void 0, error })),
|
|
18
|
+
tap(({ snapshot, error }) => {
|
|
19
|
+
entry.snapshot = snapshot, entry.error = error;
|
|
20
|
+
}),
|
|
21
|
+
// Note: any value or error emitted by the provided observable will be mapped to the cache entry's mutable state
|
|
22
|
+
// and the observable is thereafter only used as a notifier to call `onStoreChange`, hence the `void` return type.
|
|
23
|
+
map((value) => {
|
|
24
|
+
}),
|
|
25
|
+
// Ensure that the cache entry is deleted when the observable completes or errors.
|
|
26
|
+
finalize(() => cache.delete(observable)),
|
|
27
|
+
share({ resetOnRefCountZero: () => timer(0, asapScheduler) })
|
|
28
|
+
), entry.observable.subscribe().unsubscribe(), cache.set(observable, entry);
|
|
29
|
+
}
|
|
15
30
|
const store = useMemo(() => {
|
|
16
|
-
if (!cache.has(observable)) {
|
|
17
|
-
const entry = {
|
|
18
|
-
snapshot: initialValueRef.current
|
|
19
|
-
};
|
|
20
|
-
entry.observable = observable.pipe(
|
|
21
|
-
map((value) => ({ snapshot: value, error: void 0 })),
|
|
22
|
-
catchError((error) => of({ snapshot: void 0, error })),
|
|
23
|
-
tap(({ snapshot, error }) => {
|
|
24
|
-
entry.snapshot = snapshot, entry.error = error;
|
|
25
|
-
}),
|
|
26
|
-
// Note: any value or error emitted by the provided observable will be mapped to the cache entry's mutable state
|
|
27
|
-
// and the observable is thereafter only used as a notifier to call `onStoreChange`, hence the `void` return type.
|
|
28
|
-
map((value) => {
|
|
29
|
-
}),
|
|
30
|
-
// Ensure that the cache entry is deleted when the observable completes or errors.
|
|
31
|
-
finalize(() => cache.delete(observable)),
|
|
32
|
-
share()
|
|
33
|
-
), entry.subscription = entry.observable.subscribe(), cache.set(observable, entry);
|
|
34
|
-
}
|
|
35
31
|
const instance = cache.get(observable);
|
|
36
|
-
return
|
|
32
|
+
return {
|
|
37
33
|
subscribe: (onStoreChange) => {
|
|
38
34
|
const subscription = instance.observable.subscribe(onStoreChange);
|
|
39
|
-
return
|
|
35
|
+
return () => {
|
|
40
36
|
subscription.unsubscribe();
|
|
41
37
|
};
|
|
42
38
|
},
|
|
@@ -50,15 +46,15 @@ function useObservable(observable, initialValue) {
|
|
|
50
46
|
return useSyncExternalStore(
|
|
51
47
|
store.subscribe,
|
|
52
48
|
store.getSnapshot,
|
|
53
|
-
typeof
|
|
49
|
+
typeof initialValue > "u" ? void 0 : () => getValue(initialValue)
|
|
54
50
|
);
|
|
55
51
|
}
|
|
56
|
-
function useObservableEvent(
|
|
57
|
-
const [[calls$, call]] = useState(() => observableCallback()),
|
|
52
|
+
function useObservableEvent(handleEvent) {
|
|
53
|
+
const [[calls$, call]] = useState(() => observableCallback()), onEvent = useEffectEvent((observable) => handleEvent(observable));
|
|
58
54
|
return useEffect(() => {
|
|
59
|
-
const subscription = calls$.pipe(
|
|
55
|
+
const subscription = calls$.pipe((observable) => onEvent(observable)).subscribe();
|
|
60
56
|
return () => subscription.unsubscribe();
|
|
61
|
-
}, [
|
|
57
|
+
}, [calls$, onEvent]), call;
|
|
62
58
|
}
|
|
63
59
|
export {
|
|
64
60
|
useObservable,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/useObservable.ts","../src/useObservableEvent.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/useObservable.ts","../src/useObservableEvent.ts"],"sourcesContent":["import {useMemo, 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 if (!cache.has(observable)) {\n const entry: Partial<CacheRecord<ObservedValueOf<ObservableType>>> = {\n snapshot: getValue(initialValue) as ObservedValueOf<ObservableType>,\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\n const store = useMemo(() => {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const instance = cache.get(observable)!\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 initialValue === 'undefined'\n ? undefined\n : () => getValue(initialValue) as ObservedValueOf<ObservableType>,\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,MAAU,IAAA;AACjD;AAQA,MAAM,4BAAY,QAA2C;AAiB7C,SAAA,cACd,YACA,cACgD;AAChD,MAAI,CAAC,MAAM,IAAI,UAAU,GAAG;AAC1B,UAAM,QAA+D;AAAA,MACnE,UAAU,SAAS,YAAY;AAAA,IACjC;AACA,UAAM,aAAa,WAAW;AAAA,MAC5B,IAAI,CAAC,WAAW,EAAC,UAAU,OAAO,OAAO,SAAW;AAAA,MACpD,WAAW,CAAC,UAAU,GAAG,EAAC,UAAU,QAAW,MAAK,CAAC,CAAC;AAAA,MACtD,IAAI,CAAC,EAAC,UAAU,YAAW;AACnB,cAAA,WAAW,UACjB,MAAM,QAAQ;AAAA,MAAA,CACf;AAAA;AAAA;AAAA,MAGD,IAAI,CAAC,UAAO;AAAA,MAAA,CAAa;AAAA;AAAA,MAEzB,SAAS,MAAM,MAAM,OAAO,UAAU,CAAC;AAAA,MACvC,MAAM,EAAC,qBAAqB,MAAM,MAAM,GAAG,aAAa,EAAE,CAAA;AAAA,IAAA,GAIvC,MAAM,WAAW,YACzB,YAEb,GAAA,MAAM,IAAI,YAAY,KAAqD;AAAA,EAAA;AAGvE,QAAA,QAAQ,QAAQ,MAAM;AAEpB,UAAA,WAAW,MAAM,IAAI,UAAU;AAC9B,WAAA;AAAA,MACL,WAAW,CAAC,kBAA8B;AACxC,cAAM,eAAe,SAAS,WAAW,UAAU,aAAa;AAChE,eAAO,MAAM;AACX,uBAAa,YAAY;AAAA,QAC3B;AAAA,MACF;AAAA,MACA,aAAa,MAAM;AACjB,YAAI,SAAS;AACX,gBAAM,SAAS;AAEjB,eAAO,SAAS;AAAA,MAAA;AAAA,IAEpB;AAAA,EAAA,GACC,CAAC,UAAU,CAAC;AAER,SAAA;AAAA,IACL,MAAM;AAAA,IACN,MAAM;AAAA,IACN,OAAO,eAAiB,MACpB,SACA,MAAM,SAAS,YAAY;AAAA,EACjC;AACF;AC1FO,SAAS,mBACd,aACkB;AAClB,QAAM,CAAC,CAAC,QAAQ,IAAI,CAAC,IAAI,SAAS,MAAM,mBAAuB,CAAA,GAEzD,UAAU,eAAe,CAAC,eAA8B,YAAY,UAAU,CAAC;AAErF,SAAA,UAAU,MAAM;AACR,UAAA,eAAe,OAAO,KAAK,CAAC,eAAe,QAAQ,UAAU,CAAC,EAAE,UAAU;AACzE,WAAA,MAAM,aAAa,YAAY;AAAA,EACrC,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-canary.0",
|
|
4
4
|
"description": "React + RxJS = <3",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"action",
|
|
@@ -62,15 +62,6 @@
|
|
|
62
62
|
"dist",
|
|
63
63
|
"src"
|
|
64
64
|
],
|
|
65
|
-
"scripts": {
|
|
66
|
-
"build": "pkg build --strict --clean --check",
|
|
67
|
-
"dev": "pnpm --filter 'react-rx-website' dev",
|
|
68
|
-
"format": "prettier --cache --write .",
|
|
69
|
-
"lint": "eslint --cache .",
|
|
70
|
-
"prepublishOnly": "pnpm build",
|
|
71
|
-
"test": "vitest run --typecheck",
|
|
72
|
-
"watch": "pnpm build -- --watch"
|
|
73
|
-
},
|
|
74
65
|
"browserslist": "extends @sanity/browserslist-config",
|
|
75
66
|
"prettier": "@sanity/prettier-config",
|
|
76
67
|
"dependencies": {
|
|
@@ -78,36 +69,43 @@
|
|
|
78
69
|
"use-effect-event": "^1.0.2"
|
|
79
70
|
},
|
|
80
71
|
"devDependencies": {
|
|
81
|
-
"@sanity/pkg-utils": "^6.
|
|
82
|
-
"@sanity/prettier-config": "^1.0.
|
|
72
|
+
"@sanity/pkg-utils": "^6.11.7",
|
|
73
|
+
"@sanity/prettier-config": "^1.0.3",
|
|
83
74
|
"@sanity/semantic-release-preset": "^5.0.0",
|
|
84
|
-
"@testing-library/dom": "^10.
|
|
85
|
-
"@testing-library/react": "^16.0.
|
|
75
|
+
"@testing-library/dom": "^10.4.0",
|
|
76
|
+
"@testing-library/react": "^16.0.1",
|
|
86
77
|
"@types/node": "^18.17.5",
|
|
87
|
-
"@types/react": "^18.3.
|
|
88
|
-
"@types/react-dom": "^18.3.
|
|
89
|
-
"@typescript-eslint/eslint-plugin": "^
|
|
90
|
-
"@typescript-eslint/parser": "^
|
|
91
|
-
"eslint": "^8.57.
|
|
78
|
+
"@types/react": "^18.3.12",
|
|
79
|
+
"@types/react-dom": "^18.3.1",
|
|
80
|
+
"@typescript-eslint/eslint-plugin": "^8.12.2",
|
|
81
|
+
"@typescript-eslint/parser": "^8.12.2",
|
|
82
|
+
"eslint": "^8.57.1",
|
|
92
83
|
"eslint-config-prettier": "^9.1.0",
|
|
93
|
-
"eslint-plugin-prettier": "^5.1
|
|
94
|
-
"eslint-plugin-react": "^7.
|
|
95
|
-
"eslint-plugin-react-compiler": "
|
|
96
|
-
"eslint-plugin-react-hooks": "^
|
|
84
|
+
"eslint-plugin-prettier": "^5.2.1",
|
|
85
|
+
"eslint-plugin-react": "^7.37.2",
|
|
86
|
+
"eslint-plugin-react-compiler": "beta",
|
|
87
|
+
"eslint-plugin-react-hooks": "^5.0.0",
|
|
97
88
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
98
89
|
"jsdom": "^24.1.0",
|
|
99
|
-
"prettier": "^3.3.
|
|
90
|
+
"prettier": "^3.3.3",
|
|
100
91
|
"react": "^18.3.1",
|
|
101
92
|
"react-dom": "^18.3.1",
|
|
102
93
|
"react-test-renderer": "^18.3.1",
|
|
103
94
|
"rxjs": "^7.8.1",
|
|
104
|
-
"semantic-release": "^24.
|
|
105
|
-
"typescript": "5.
|
|
106
|
-
"vitest": "^2.
|
|
95
|
+
"semantic-release": "^24.1.0",
|
|
96
|
+
"typescript": "5.6.3",
|
|
97
|
+
"vitest": "^2.1.4"
|
|
107
98
|
},
|
|
108
99
|
"peerDependencies": {
|
|
109
100
|
"react": "^18.3 || >=19.0.0-rc",
|
|
110
101
|
"rxjs": "^7"
|
|
111
102
|
},
|
|
112
|
-
"
|
|
113
|
-
|
|
103
|
+
"scripts": {
|
|
104
|
+
"build": "pkg build --strict --clean --check",
|
|
105
|
+
"dev": "pnpm --filter 'react-rx-website' dev",
|
|
106
|
+
"format": "prettier --cache --write .",
|
|
107
|
+
"lint": "eslint --cache .",
|
|
108
|
+
"test": "vitest run --typecheck",
|
|
109
|
+
"watch": "pnpm build -- --watch"
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -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
|
-
import {
|
|
1
|
+
import {useMemo, 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,61 +35,45 @@ 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>(
|
|
42
42
|
observable: ObservableType,
|
|
43
43
|
initialValue?: InitialValue | (() => InitialValue),
|
|
44
44
|
): InitialValue | ObservedValueOf<ObservableType> {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
45
|
+
if (!cache.has(observable)) {
|
|
46
|
+
const entry: Partial<CacheRecord<ObservedValueOf<ObservableType>>> = {
|
|
47
|
+
snapshot: getValue(initialValue) as ObservedValueOf<ObservableType>,
|
|
48
|
+
}
|
|
49
|
+
entry.observable = observable.pipe(
|
|
50
|
+
map((value) => ({snapshot: value, error: undefined})),
|
|
51
|
+
catchError((error) => of({snapshot: undefined, error})),
|
|
52
|
+
tap(({snapshot, error}) => {
|
|
53
|
+
entry.snapshot = snapshot
|
|
54
|
+
entry.error = error
|
|
55
|
+
}),
|
|
56
|
+
// Note: any value or error emitted by the provided observable will be mapped to the cache entry's mutable state
|
|
57
|
+
// and the observable is thereafter only used as a notifier to call `onStoreChange`, hence the `void` return type.
|
|
58
|
+
map((value) => void value),
|
|
59
|
+
// Ensure that the cache entry is deleted when the observable completes or errors.
|
|
60
|
+
finalize(() => cache.delete(observable)),
|
|
61
|
+
share({resetOnRefCountZero: () => timer(0, asapScheduler)}),
|
|
62
|
+
)
|
|
57
63
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
snapshot: initialValueRef.current,
|
|
62
|
-
}
|
|
63
|
-
entry.observable = observable.pipe(
|
|
64
|
-
map((value) => ({snapshot: value, error: undefined})),
|
|
65
|
-
catchError((error) => of({snapshot: undefined, error})),
|
|
66
|
-
tap(({snapshot, error}) => {
|
|
67
|
-
entry.snapshot = snapshot
|
|
68
|
-
entry.error = error
|
|
69
|
-
}),
|
|
70
|
-
// Note: any value or error emitted by the provided observable will be mapped to the cache entry's mutable state
|
|
71
|
-
// and the observable is thereafter only used as a notifier to call `onStoreChange`, hence the `void` return type.
|
|
72
|
-
map((value) => void value),
|
|
73
|
-
// Ensure that the cache entry is deleted when the observable completes or errors.
|
|
74
|
-
finalize(() => cache.delete(observable)),
|
|
75
|
-
share(),
|
|
76
|
-
)
|
|
64
|
+
// Eagerly subscribe to sync set `entry.currentValue` to what the observable returns, and keep the observable alive until the component unmounts.
|
|
65
|
+
const subscription = entry.observable.subscribe()
|
|
66
|
+
subscription.unsubscribe()
|
|
77
67
|
|
|
78
|
-
|
|
79
|
-
|
|
68
|
+
cache.set(observable, entry as CacheRecord<ObservedValueOf<ObservableType>>)
|
|
69
|
+
}
|
|
80
70
|
|
|
81
|
-
|
|
82
|
-
}
|
|
71
|
+
const store = useMemo(() => {
|
|
83
72
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
84
73
|
const instance = cache.get(observable)!
|
|
85
|
-
if (instance.subscription.closed) {
|
|
86
|
-
instance.subscription = instance.observable.subscribe()
|
|
87
|
-
}
|
|
88
|
-
|
|
89
74
|
return {
|
|
90
75
|
subscribe: (onStoreChange: () => void) => {
|
|
91
76
|
const subscription = instance.observable.subscribe(onStoreChange)
|
|
92
|
-
instance.subscription.unsubscribe()
|
|
93
77
|
return () => {
|
|
94
78
|
subscription.unsubscribe()
|
|
95
79
|
}
|
|
@@ -106,6 +90,8 @@ export function useObservable<ObservableType extends Observable<any>, InitialVal
|
|
|
106
90
|
return useSyncExternalStore<ObservedValueOf<ObservableType>>(
|
|
107
91
|
store.subscribe,
|
|
108
92
|
store.getSnapshot,
|
|
109
|
-
typeof
|
|
93
|
+
typeof initialValue === 'undefined'
|
|
94
|
+
? undefined
|
|
95
|
+
: () => getValue(initialValue) as ObservedValueOf<ObservableType>,
|
|
110
96
|
)
|
|
111
97
|
}
|
|
@@ -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
|
}
|