react-rx 3.0.0-5 → 3.0.0-7
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/LICENSE +1 -1
- package/dist/index.cjs +70 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +26 -0
- package/dist/index.d.ts +13 -4
- package/dist/index.js +39 -19
- package/dist/index.js.map +1 -1
- package/package.json +21 -24
- package/src/__tests__/errors.test.tsx +51 -0
- package/src/__tests__/strictmode.test.ts +1 -0
- package/src/__tests__/useObservable.test-d.ts +27 -0
- package/src/__tests__/{useObservable.test.tsx → useObservable.test.ts} +3 -2
- package/src/useObservable.ts +47 -15
- package/src/useObservableEvent.ts +3 -1
- package/dist/index.d.mts +0 -17
- package/dist/index.mjs +0 -54
- package/dist/index.mjs.map +0 -1
package/LICENSE
CHANGED
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: !0 });
|
|
3
|
+
var react = require("react"), operators = require("rxjs/operators"), observableCallback = require("observable-callback");
|
|
4
|
+
function getValue(value) {
|
|
5
|
+
return typeof value == "function" ? value() : value;
|
|
6
|
+
}
|
|
7
|
+
const cache = /* @__PURE__ */ new WeakMap();
|
|
8
|
+
function useObservable(observable, initialValue) {
|
|
9
|
+
const initialValueRef = react.useRef(getValue(initialValue));
|
|
10
|
+
react.useEffect(() => {
|
|
11
|
+
initialValueRef.current = getValue(initialValue);
|
|
12
|
+
}, [initialValue]);
|
|
13
|
+
const store = react.useMemo(() => {
|
|
14
|
+
if (!cache.has(observable)) {
|
|
15
|
+
const entry = {
|
|
16
|
+
snapshot: initialValueRef.current
|
|
17
|
+
};
|
|
18
|
+
entry.observable = observable.pipe(
|
|
19
|
+
operators.shareReplay({ refCount: !0, bufferSize: 1 }),
|
|
20
|
+
operators.tap({
|
|
21
|
+
next: (value) => {
|
|
22
|
+
entry.snapshot = value, entry.error = void 0;
|
|
23
|
+
},
|
|
24
|
+
error: (error) => entry.error = error
|
|
25
|
+
})
|
|
26
|
+
), entry.subscription = entry.observable.subscribe(), cache.set(observable, entry);
|
|
27
|
+
}
|
|
28
|
+
const instance = cache.get(observable);
|
|
29
|
+
return instance.subscription.closed && (instance.subscription = instance.observable.subscribe()), {
|
|
30
|
+
subscribe: (onStoreChange) => {
|
|
31
|
+
const subscription = instance.observable.subscribe({
|
|
32
|
+
next: onStoreChange,
|
|
33
|
+
error: onStoreChange
|
|
34
|
+
});
|
|
35
|
+
return instance.subscription.unsubscribe(), () => {
|
|
36
|
+
subscription.unsubscribe();
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
getSnapshot: () => {
|
|
40
|
+
if (instance.error)
|
|
41
|
+
throw instance.error;
|
|
42
|
+
return instance.snapshot;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}, [observable]);
|
|
46
|
+
return react.useSyncExternalStore(
|
|
47
|
+
store.subscribe,
|
|
48
|
+
store.getSnapshot,
|
|
49
|
+
typeof initialValueRef.current > "u" ? void 0 : () => initialValueRef.current
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
function useObservableEvent(fn) {
|
|
53
|
+
const [[calls$, call]] = react.useState(() => observableCallback.observableCallback()), callback = useEffectEvent(fn);
|
|
54
|
+
return react.useEffect(() => {
|
|
55
|
+
const subscription = calls$.pipe(callback).subscribe();
|
|
56
|
+
return () => subscription.unsubscribe();
|
|
57
|
+
}, [callback, calls$]), call;
|
|
58
|
+
}
|
|
59
|
+
function useEffectEvent(fn) {
|
|
60
|
+
const ref = react.useRef(null);
|
|
61
|
+
return react.useInsertionEffect(() => {
|
|
62
|
+
ref.current = fn;
|
|
63
|
+
}, [fn]), react.useCallback((...args) => {
|
|
64
|
+
const f = ref.current;
|
|
65
|
+
return f(...args);
|
|
66
|
+
}, []);
|
|
67
|
+
}
|
|
68
|
+
exports.useObservable = useObservable;
|
|
69
|
+
exports.useObservableEvent = useObservableEvent;
|
|
70
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/useObservable.ts","../src/useObservableEvent.ts"],"sourcesContent":["import {useEffect, useMemo, useRef, useSyncExternalStore} from 'react'\nimport {type Observable, type ObservedValueOf, type Subscription} from 'rxjs'\nimport {shareReplay, tap} from 'rxjs/operators'\n\nfunction getValue<T>(value: T): T extends () => infer U ? U : T {\n return typeof value === 'function' ? value() : value\n}\ninterface CacheRecord<T> {\n subscription: Subscription\n observable: Observable<T>\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,\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 shareReplay({refCount: true, bufferSize: 1}),\n tap({\n next: (value) => {\n entry.snapshot = value\n entry.error = undefined\n },\n error: (error: unknown) => (entry.error = error),\n }),\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 entry.subscription = entry.observable.subscribe()\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 if (instance.subscription.closed) {\n instance.subscription = instance.observable.subscribe()\n }\n\n return {\n subscribe: (onStoreChange: () => void) => {\n const subscription = instance.observable.subscribe({\n next: onStoreChange,\n error: onStoreChange,\n })\n instance.subscription.unsubscribe()\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 {useCallback, useEffect, useInsertionEffect, useRef, useState} from 'react'\nimport {type Observable} from 'rxjs'\n\n/** @public */\nexport function useObservableEvent<T, U>(\n fn: (arg: Observable<T>) => Observable<U>,\n): (arg: T) => void {\n const [[calls$, call]] = useState(() => observableCallback<T>())\n\n const callback = useEffectEvent(fn)\n\n useEffect(() => {\n const subscription = calls$.pipe(callback).subscribe()\n return () => subscription.unsubscribe()\n }, [callback, calls$])\n\n return call\n}\n\n/**\n * This is a ponyfill of the upcoming `useEffectEvent` hook that'll arrive in React 19.\n * https://19.react.dev/learn/separating-events-from-effects#declaring-an-effect-event\n * To learn more about the ponyfill itself, see: https://blog.bitsrc.io/a-look-inside-the-useevent-polyfill-from-the-new-react-docs-d1c4739e8072\n */\nfunction useEffectEvent<\n const T extends (\n ...args: // eslint-disable-next-line @typescript-eslint/no-explicit-any\n any[]\n ) => void,\n>(fn: T): T {\n const ref = useRef<T | null>(null)\n useInsertionEffect(() => {\n ref.current = fn\n }, [fn])\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return useCallback((...args: any[]) => {\n const f = ref.current!\n return f(...args)\n }, []) as T\n}\n"],"names":["useRef","useEffect","useMemo","shareReplay","tap","useSyncExternalStore","useState","observableCallback","useInsertionEffect","useCallback"],"mappings":";;;AAIA,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,sBAAY,EAAC,UAAU,IAAM,YAAY,GAAE;AAAA,QAC3CC,cAAI;AAAA,UACF,MAAM,CAAC,UAAU;AACT,kBAAA,WAAW,OACjB,MAAM,QAAQ;AAAA,UAChB;AAAA,UACA,OAAO,CAAC,UAAoB,MAAM,QAAQ;AAAA,QAAA,CAC3C;AAAA,MACH,GAGA,MAAM,eAAe,MAAM,WAAW,aAEtC,MAAM,IAAI,YAAY,KAAqD;AAAA,IAC7E;AAEM,UAAA,WAAW,MAAM,IAAI,UAAU;AACjC,WAAA,SAAS,aAAa,WACxB,SAAS,eAAe,SAAS,WAAW,cAGvC;AAAA,MACL,WAAW,CAAC,kBAA8B;AAClC,cAAA,eAAe,SAAS,WAAW,UAAU;AAAA,UACjD,MAAM;AAAA,UACN,OAAO;AAAA,QAAA,CACR;AACQ,eAAA,SAAA,aAAa,YAAY,GAC3B,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;AC/FO,SAAS,mBACd,IACkB;AAClB,QAAM,CAAC,CAAC,QAAQ,IAAI,CAAC,IAAIC,MAAS,SAAA,MAAMC,mBAAsB,mBAAA,CAAC,GAEzD,WAAW,eAAe,EAAE;AAElC,SAAAN,MAAA,UAAU,MAAM;AACd,UAAM,eAAe,OAAO,KAAK,QAAQ,EAAE,UAAU;AAC9C,WAAA,MAAM,aAAa;EACzB,GAAA,CAAC,UAAU,MAAM,CAAC,GAEd;AACT;AAOA,SAAS,eAKP,IAAU;AACJ,QAAA,MAAMD,aAAiB,IAAI;AACjC,SAAAQ,MAAA,mBAAmB,MAAM;AACvB,QAAI,UAAU;AAAA,KACb,CAAC,EAAE,CAAC,GAEAC,MAAAA,YAAY,IAAI,SAAgB;AACrC,UAAM,IAAI,IAAI;AACP,WAAA,EAAE,GAAG,IAAI;AAAA,EAClB,GAAG,CAAE,CAAA;AACP;;;"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {Observable} from 'rxjs'
|
|
2
|
+
import {ObservedValueOf} from 'rxjs'
|
|
3
|
+
|
|
4
|
+
/** @public */
|
|
5
|
+
export declare function useObservable<ObservableType extends Observable<any>>(
|
|
6
|
+
observable: ObservableType,
|
|
7
|
+
initialValue: ObservedValueOf<ObservableType> | (() => ObservedValueOf<ObservableType>),
|
|
8
|
+
): ObservedValueOf<ObservableType>
|
|
9
|
+
|
|
10
|
+
/** @public */
|
|
11
|
+
export declare function useObservable<ObservableType extends Observable<any>>(
|
|
12
|
+
observable: ObservableType,
|
|
13
|
+
): undefined | ObservedValueOf<ObservableType>
|
|
14
|
+
|
|
15
|
+
/** @public */
|
|
16
|
+
export declare function useObservable<ObservableType extends Observable<any>, InitialValue>(
|
|
17
|
+
observable: ObservableType,
|
|
18
|
+
initialValue: InitialValue,
|
|
19
|
+
): InitialValue | ObservedValueOf<ObservableType>
|
|
20
|
+
|
|
21
|
+
/** @public */
|
|
22
|
+
export declare function useObservableEvent<T, U>(
|
|
23
|
+
fn: (arg: Observable<T>) => Observable<U>,
|
|
24
|
+
): (arg: T) => void
|
|
25
|
+
|
|
26
|
+
export {}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import {Observable} from 'rxjs'
|
|
2
|
+
import {ObservedValueOf} from 'rxjs'
|
|
2
3
|
|
|
3
|
-
/** @
|
|
4
|
-
export declare
|
|
4
|
+
/** @public */
|
|
5
|
+
export declare function useObservable<ObservableType extends Observable<any>>(
|
|
6
|
+
observable: ObservableType,
|
|
7
|
+
initialValue: ObservedValueOf<ObservableType> | (() => ObservedValueOf<ObservableType>),
|
|
8
|
+
): ObservedValueOf<ObservableType>
|
|
5
9
|
|
|
6
10
|
/** @public */
|
|
7
11
|
export declare function useObservable<ObservableType extends Observable<any>>(
|
|
8
12
|
observable: ObservableType,
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
): undefined | ObservedValueOf<ObservableType>
|
|
14
|
+
|
|
15
|
+
/** @public */
|
|
16
|
+
export declare function useObservable<ObservableType extends Observable<any>, InitialValue>(
|
|
17
|
+
observable: ObservableType,
|
|
18
|
+
initialValue: InitialValue,
|
|
19
|
+
): InitialValue | ObservedValueOf<ObservableType>
|
|
11
20
|
|
|
12
21
|
/** @public */
|
|
13
22
|
export declare function useObservableEvent<T, U>(
|
package/dist/index.js
CHANGED
|
@@ -1,52 +1,72 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { useRef, useEffect, useMemo, useSyncExternalStore, useState, useInsertionEffect, useCallback } from "react";
|
|
2
|
+
import { shareReplay, tap } from "rxjs/operators";
|
|
3
|
+
import { observableCallback } from "observable-callback";
|
|
4
4
|
function getValue(value) {
|
|
5
5
|
return typeof value == "function" ? value() : value;
|
|
6
6
|
}
|
|
7
7
|
const cache = /* @__PURE__ */ new WeakMap();
|
|
8
8
|
function useObservable(observable, initialValue) {
|
|
9
|
-
const initialValueRef =
|
|
10
|
-
|
|
9
|
+
const initialValueRef = useRef(getValue(initialValue));
|
|
10
|
+
useEffect(() => {
|
|
11
11
|
initialValueRef.current = getValue(initialValue);
|
|
12
12
|
}, [initialValue]);
|
|
13
|
-
const store =
|
|
13
|
+
const store = useMemo(() => {
|
|
14
14
|
if (!cache.has(observable)) {
|
|
15
15
|
const entry = {
|
|
16
16
|
snapshot: initialValueRef.current
|
|
17
17
|
};
|
|
18
18
|
entry.observable = observable.pipe(
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
shareReplay({ refCount: !0, bufferSize: 1 }),
|
|
20
|
+
tap({
|
|
21
|
+
next: (value) => {
|
|
22
|
+
entry.snapshot = value, entry.error = void 0;
|
|
23
|
+
},
|
|
24
|
+
error: (error) => entry.error = error
|
|
25
|
+
})
|
|
21
26
|
), entry.subscription = entry.observable.subscribe(), cache.set(observable, entry);
|
|
22
27
|
}
|
|
23
28
|
const instance = cache.get(observable);
|
|
24
29
|
return instance.subscription.closed && (instance.subscription = instance.observable.subscribe()), {
|
|
25
30
|
subscribe: (onStoreChange) => {
|
|
26
|
-
const subscription = instance.observable.subscribe(
|
|
27
|
-
|
|
31
|
+
const subscription = instance.observable.subscribe({
|
|
32
|
+
next: onStoreChange,
|
|
33
|
+
error: onStoreChange
|
|
34
|
+
});
|
|
35
|
+
return instance.subscription.unsubscribe(), () => {
|
|
36
|
+
subscription.unsubscribe();
|
|
37
|
+
};
|
|
28
38
|
},
|
|
29
|
-
getSnapshot: () =>
|
|
39
|
+
getSnapshot: () => {
|
|
40
|
+
if (instance.error)
|
|
41
|
+
throw instance.error;
|
|
42
|
+
return instance.snapshot;
|
|
43
|
+
}
|
|
30
44
|
};
|
|
31
45
|
}, [observable]);
|
|
32
|
-
return
|
|
46
|
+
return useSyncExternalStore(
|
|
47
|
+
store.subscribe,
|
|
48
|
+
store.getSnapshot,
|
|
49
|
+
typeof initialValueRef.current > "u" ? void 0 : () => initialValueRef.current
|
|
50
|
+
);
|
|
33
51
|
}
|
|
34
52
|
function useObservableEvent(fn) {
|
|
35
|
-
const [[calls$, call]] =
|
|
36
|
-
return
|
|
53
|
+
const [[calls$, call]] = useState(() => observableCallback()), callback = useEffectEvent(fn);
|
|
54
|
+
return useEffect(() => {
|
|
37
55
|
const subscription = calls$.pipe(callback).subscribe();
|
|
38
56
|
return () => subscription.unsubscribe();
|
|
39
57
|
}, [callback, calls$]), call;
|
|
40
58
|
}
|
|
41
59
|
function useEffectEvent(fn) {
|
|
42
|
-
const ref =
|
|
43
|
-
return
|
|
60
|
+
const ref = useRef(null);
|
|
61
|
+
return useInsertionEffect(() => {
|
|
44
62
|
ref.current = fn;
|
|
45
|
-
}, [fn]),
|
|
63
|
+
}, [fn]), useCallback((...args) => {
|
|
46
64
|
const f = ref.current;
|
|
47
65
|
return f(...args);
|
|
48
66
|
}, []);
|
|
49
67
|
}
|
|
50
|
-
|
|
51
|
-
|
|
68
|
+
export {
|
|
69
|
+
useObservable,
|
|
70
|
+
useObservableEvent
|
|
71
|
+
};
|
|
52
72
|
//# sourceMappingURL=index.js.map
|
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 type
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/useObservable.ts","../src/useObservableEvent.ts"],"sourcesContent":["import {useEffect, useMemo, useRef, useSyncExternalStore} from 'react'\nimport {type Observable, type ObservedValueOf, type Subscription} from 'rxjs'\nimport {shareReplay, tap} from 'rxjs/operators'\n\nfunction getValue<T>(value: T): T extends () => infer U ? U : T {\n return typeof value === 'function' ? value() : value\n}\ninterface CacheRecord<T> {\n subscription: Subscription\n observable: Observable<T>\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,\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 shareReplay({refCount: true, bufferSize: 1}),\n tap({\n next: (value) => {\n entry.snapshot = value\n entry.error = undefined\n },\n error: (error: unknown) => (entry.error = error),\n }),\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 entry.subscription = entry.observable.subscribe()\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 if (instance.subscription.closed) {\n instance.subscription = instance.observable.subscribe()\n }\n\n return {\n subscribe: (onStoreChange: () => void) => {\n const subscription = instance.observable.subscribe({\n next: onStoreChange,\n error: onStoreChange,\n })\n instance.subscription.unsubscribe()\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 {useCallback, useEffect, useInsertionEffect, useRef, useState} from 'react'\nimport {type Observable} from 'rxjs'\n\n/** @public */\nexport function useObservableEvent<T, U>(\n fn: (arg: Observable<T>) => Observable<U>,\n): (arg: T) => void {\n const [[calls$, call]] = useState(() => observableCallback<T>())\n\n const callback = useEffectEvent(fn)\n\n useEffect(() => {\n const subscription = calls$.pipe(callback).subscribe()\n return () => subscription.unsubscribe()\n }, [callback, calls$])\n\n return call\n}\n\n/**\n * This is a ponyfill of the upcoming `useEffectEvent` hook that'll arrive in React 19.\n * https://19.react.dev/learn/separating-events-from-effects#declaring-an-effect-event\n * To learn more about the ponyfill itself, see: https://blog.bitsrc.io/a-look-inside-the-useevent-polyfill-from-the-new-react-docs-d1c4739e8072\n */\nfunction useEffectEvent<\n const T extends (\n ...args: // eslint-disable-next-line @typescript-eslint/no-explicit-any\n any[]\n ) => void,\n>(fn: T): T {\n const ref = useRef<T | null>(null)\n useInsertionEffect(() => {\n ref.current = fn\n }, [fn])\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return useCallback((...args: any[]) => {\n const f = ref.current!\n return f(...args)\n }, []) as T\n}\n"],"names":[],"mappings":";;;AAIA,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,YAAY,EAAC,UAAU,IAAM,YAAY,GAAE;AAAA,QAC3C,IAAI;AAAA,UACF,MAAM,CAAC,UAAU;AACT,kBAAA,WAAW,OACjB,MAAM,QAAQ;AAAA,UAChB;AAAA,UACA,OAAO,CAAC,UAAoB,MAAM,QAAQ;AAAA,QAAA,CAC3C;AAAA,MACH,GAGA,MAAM,eAAe,MAAM,WAAW,aAEtC,MAAM,IAAI,YAAY,KAAqD;AAAA,IAC7E;AAEM,UAAA,WAAW,MAAM,IAAI,UAAU;AACjC,WAAA,SAAS,aAAa,WACxB,SAAS,eAAe,SAAS,WAAW,cAGvC;AAAA,MACL,WAAW,CAAC,kBAA8B;AAClC,cAAA,eAAe,SAAS,WAAW,UAAU;AAAA,UACjD,MAAM;AAAA,UACN,OAAO;AAAA,QAAA,CACR;AACQ,eAAA,SAAA,aAAa,YAAY,GAC3B,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;AC/FO,SAAS,mBACd,IACkB;AAClB,QAAM,CAAC,CAAC,QAAQ,IAAI,CAAC,IAAI,SAAS,MAAM,mBAAsB,CAAC,GAEzD,WAAW,eAAe,EAAE;AAElC,SAAA,UAAU,MAAM;AACd,UAAM,eAAe,OAAO,KAAK,QAAQ,EAAE,UAAU;AAC9C,WAAA,MAAM,aAAa;EACzB,GAAA,CAAC,UAAU,MAAM,CAAC,GAEd;AACT;AAOA,SAAS,eAKP,IAAU;AACJ,QAAA,MAAM,OAAiB,IAAI;AACjC,SAAA,mBAAmB,MAAM;AACvB,QAAI,UAAU;AAAA,KACb,CAAC,EAAE,CAAC,GAEA,YAAY,IAAI,SAAgB;AACrC,UAAM,IAAI,IAAI;AACP,WAAA,EAAE,GAAG,IAAI;AAAA,EAClB,GAAG,CAAE,CAAA;AACP;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-rx",
|
|
3
|
-
"version": "3.0.0-
|
|
3
|
+
"version": "3.0.0-7",
|
|
4
4
|
"description": "React + RxJS = <3",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"action",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"pipe",
|
|
16
16
|
"react",
|
|
17
17
|
"react18",
|
|
18
|
+
"react19",
|
|
18
19
|
"reactive",
|
|
19
20
|
"realtime",
|
|
20
21
|
"rx",
|
|
@@ -28,7 +29,6 @@
|
|
|
28
29
|
"sync",
|
|
29
30
|
"typesafe",
|
|
30
31
|
"typescript",
|
|
31
|
-
"use-sync-external-store",
|
|
32
32
|
"use"
|
|
33
33
|
],
|
|
34
34
|
"homepage": "https://react-rx.dev",
|
|
@@ -46,17 +46,17 @@
|
|
|
46
46
|
"Cody Olsen <stipsan@gmail.com> (https://github.com/stipsan)"
|
|
47
47
|
],
|
|
48
48
|
"sideEffects": false,
|
|
49
|
-
"type": "
|
|
49
|
+
"type": "module",
|
|
50
50
|
"exports": {
|
|
51
51
|
".": {
|
|
52
52
|
"source": "./src/index.ts",
|
|
53
|
-
"
|
|
53
|
+
"require": "./dist/index.cjs",
|
|
54
54
|
"default": "./dist/index.js"
|
|
55
55
|
},
|
|
56
56
|
"./package.json": "./package.json"
|
|
57
57
|
},
|
|
58
|
-
"main": "./dist/index.
|
|
59
|
-
"module": "./dist/index.
|
|
58
|
+
"main": "./dist/index.cjs",
|
|
59
|
+
"module": "./dist/index.js",
|
|
60
60
|
"types": "./dist/index.d.ts",
|
|
61
61
|
"files": [
|
|
62
62
|
"dist",
|
|
@@ -66,26 +66,17 @@
|
|
|
66
66
|
"website",
|
|
67
67
|
"."
|
|
68
68
|
],
|
|
69
|
-
"scripts": {
|
|
70
|
-
"build": "pkg build --strict --clean --check",
|
|
71
|
-
"dev": "cd website && npm run dev",
|
|
72
|
-
"prepublishOnly": "npm run build",
|
|
73
|
-
"watch": "npm run build -- --watch",
|
|
74
|
-
"test": "jest",
|
|
75
|
-
"lint": "eslint --cache ."
|
|
76
|
-
},
|
|
77
69
|
"browserslist": "extends @sanity/browserslist-config",
|
|
78
70
|
"prettier": "@sanity/prettier-config",
|
|
79
71
|
"dependencies": {
|
|
80
72
|
"observable-callback": "^1.0.3"
|
|
81
73
|
},
|
|
82
74
|
"devDependencies": {
|
|
83
|
-
"@sanity/pkg-utils": "^6.9.
|
|
75
|
+
"@sanity/pkg-utils": "^6.9.3",
|
|
84
76
|
"@sanity/prettier-config": "^1.0.2",
|
|
85
|
-
"@sanity/semantic-release-preset": "^
|
|
77
|
+
"@sanity/semantic-release-preset": "^4.1.8",
|
|
86
78
|
"@testing-library/dom": "^10.1.0",
|
|
87
79
|
"@testing-library/react": "^16.0.0",
|
|
88
|
-
"@types/jest": "^29.5.3",
|
|
89
80
|
"@types/node": "^18.17.5",
|
|
90
81
|
"@types/react": "^18.3.3",
|
|
91
82
|
"@types/react-dom": "^18.3.0",
|
|
@@ -98,20 +89,26 @@
|
|
|
98
89
|
"eslint-plugin-react-compiler": "0.0.0-experimental-51a85ea-20240601",
|
|
99
90
|
"eslint-plugin-react-hooks": "^4.6.2",
|
|
100
91
|
"eslint-plugin-simple-import-sort": "^12.1.0",
|
|
101
|
-
"
|
|
102
|
-
"jest-environment-jsdom": "^29.6.2",
|
|
103
|
-
"jsdom": "^20.0.3",
|
|
104
|
-
"npm-run-all": "^4.1.5",
|
|
92
|
+
"jsdom": "^24.1.0",
|
|
105
93
|
"prettier": "^3.3.1",
|
|
106
94
|
"react": "^18.3.1",
|
|
107
95
|
"react-dom": "^18.3.1",
|
|
108
96
|
"react-test-renderer": "^18.3.1",
|
|
109
97
|
"rxjs": "^7.8.1",
|
|
110
|
-
"
|
|
111
|
-
"typescript": "5.4.5"
|
|
98
|
+
"semantic-release": "^24.0.0",
|
|
99
|
+
"typescript": "5.4.5",
|
|
100
|
+
"vitest": "^1.6.0"
|
|
112
101
|
},
|
|
113
102
|
"peerDependencies": {
|
|
114
103
|
"react": "^18.3 || >=19.0.0-rc",
|
|
115
104
|
"rxjs": "^7"
|
|
105
|
+
},
|
|
106
|
+
"scripts": {
|
|
107
|
+
"build": "pkg build --strict --clean --check",
|
|
108
|
+
"dev": "pnpm --filter 'react-rx-website' dev",
|
|
109
|
+
"format": "prettier --cache --write .",
|
|
110
|
+
"lint": "eslint --cache .",
|
|
111
|
+
"test": "vitest run --typecheck",
|
|
112
|
+
"watch": "pnpm build -- --watch"
|
|
116
113
|
}
|
|
117
|
-
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {act, render} from '@testing-library/react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import {mergeMap, of, Subject, throwError} from 'rxjs'
|
|
4
|
+
import {expect, test, vitest} from 'vitest'
|
|
5
|
+
|
|
6
|
+
import {useObservable} from '../useObservable.ts'
|
|
7
|
+
|
|
8
|
+
test('errors emitted by the observable should be thrown during the react render phase', () => {
|
|
9
|
+
const subject = new Subject<{error: boolean; message: string}>()
|
|
10
|
+
|
|
11
|
+
const messages = subject
|
|
12
|
+
.asObservable()
|
|
13
|
+
.pipe(
|
|
14
|
+
mergeMap((value) =>
|
|
15
|
+
value.error ? throwError(() => new Error(value.message)) : of(value.message),
|
|
16
|
+
),
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
function ObservableComponent() {
|
|
20
|
+
return useObservable(messages, '☺️')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const {container, rerender} = render(<ObservableComponent />)
|
|
24
|
+
// no error (yet)
|
|
25
|
+
expect(container).toMatchInlineSnapshot(`
|
|
26
|
+
<div>
|
|
27
|
+
☺️
|
|
28
|
+
</div>
|
|
29
|
+
`)
|
|
30
|
+
|
|
31
|
+
act(() => {
|
|
32
|
+
// Note that the error is thrown later, during the render phase
|
|
33
|
+
subject.next({error: true, message: 'Boom'})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const consoleErrorSpy = vitest.spyOn(globalThis.console, 'error').mockImplementation(() => {
|
|
37
|
+
// silence console.error()'s
|
|
38
|
+
})
|
|
39
|
+
expect(() => rerender(<ObservableComponent />)).toThrowErrorMatchingInlineSnapshot(
|
|
40
|
+
`[Error: Boom]`,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
// Assert that react warnings are captured and logged to the console
|
|
44
|
+
// note: this may change with React version
|
|
45
|
+
expect(consoleErrorSpy.mock.calls.flat().join('\n')).toMatchObject(
|
|
46
|
+
expect.stringContaining('Uncaught [Error: Boom]'),
|
|
47
|
+
)
|
|
48
|
+
expect(consoleErrorSpy.mock.calls.flat().join('\n')).toMatchObject(
|
|
49
|
+
expect.stringContaining('The above error occurred in the <ObservableComponent> component'),
|
|
50
|
+
)
|
|
51
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {of} from 'rxjs'
|
|
2
|
+
import {expectTypeOf, test} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {useObservable} from '../useObservable'
|
|
5
|
+
|
|
6
|
+
test('useObservable with no initial value can be undefined', () => {
|
|
7
|
+
const observable = of('foo')
|
|
8
|
+
|
|
9
|
+
expectTypeOf(useObservable(observable)).toEqualTypeOf<string | undefined>()
|
|
10
|
+
|
|
11
|
+
//@ts-expect-error - because initial value is not given, we can't guarantee the observable emits a sync value, so it could be undefined
|
|
12
|
+
expectTypeOf(useObservable(observable)).toEqualTypeOf<string>()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('return type of useObservable with initial value is not undefined', () => {
|
|
16
|
+
const observable = of('foo')
|
|
17
|
+
//@ts-expect-error - because initial value is given, the return type can never be undefined
|
|
18
|
+
expectTypeOf(useObservable(observable, 'bar')).toEqualTypeOf<string | undefined>()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('useObservable with initial value if a different type returns a union of the observed type and the initial value type', () => {
|
|
22
|
+
const observable = of('foo')
|
|
23
|
+
|
|
24
|
+
expectTypeOf(useObservable(observable, 1)).toEqualTypeOf<string | number>()
|
|
25
|
+
|
|
26
|
+
expectTypeOf(useObservable(observable, 'foo')).toEqualTypeOf<string>()
|
|
27
|
+
})
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import {act, render, renderHook} from '@testing-library/react'
|
|
2
2
|
import {createElement, Fragment} from 'react'
|
|
3
3
|
import {asyncScheduler, Observable, of, scheduled, Subject, timer} from 'rxjs'
|
|
4
|
-
import {
|
|
4
|
+
import {map} from 'rxjs/operators'
|
|
5
|
+
import {expect, test} from 'vitest'
|
|
5
6
|
|
|
6
7
|
import {useObservable} from '../useObservable'
|
|
7
8
|
|
|
@@ -42,7 +43,7 @@ test('should only subscribe once when given same observable on re-renders', () =
|
|
|
42
43
|
})
|
|
43
44
|
|
|
44
45
|
test('should not return undefined during render if initial value is given', () => {
|
|
45
|
-
const observable = timer(100).pipe(
|
|
46
|
+
const observable = timer(100).pipe(map(() => 'emitted value'))
|
|
46
47
|
|
|
47
48
|
const returnedValues: unknown[] = []
|
|
48
49
|
function ObservableComponent() {
|
package/src/useObservable.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {useEffect, useMemo, useRef, useSyncExternalStore} from 'react'
|
|
2
|
-
import type
|
|
2
|
+
import {type Observable, type ObservedValueOf, type Subscription} from 'rxjs'
|
|
3
3
|
import {shareReplay, tap} from 'rxjs/operators'
|
|
4
4
|
|
|
5
5
|
function getValue<T>(value: T): T extends () => infer U ? U : T {
|
|
@@ -9,6 +9,7 @@ interface CacheRecord<T> {
|
|
|
9
9
|
subscription: Subscription
|
|
10
10
|
observable: Observable<T>
|
|
11
11
|
snapshot: T
|
|
12
|
+
error?: unknown
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
const cache = new WeakMap<Observable<any>, CacheRecord<any>>()
|
|
@@ -16,35 +17,55 @@ const cache = new WeakMap<Observable<any>, CacheRecord<any>>()
|
|
|
16
17
|
/** @public */
|
|
17
18
|
export function useObservable<ObservableType extends Observable<any>>(
|
|
18
19
|
observable: ObservableType,
|
|
19
|
-
initialValue
|
|
20
|
-
):
|
|
20
|
+
initialValue: ObservedValueOf<ObservableType> | (() => ObservedValueOf<ObservableType>),
|
|
21
|
+
): ObservedValueOf<ObservableType>
|
|
22
|
+
/** @public */
|
|
23
|
+
export function useObservable<ObservableType extends Observable<any>>(
|
|
24
|
+
observable: ObservableType,
|
|
25
|
+
): undefined | ObservedValueOf<ObservableType>
|
|
26
|
+
/** @public */
|
|
27
|
+
export function useObservable<ObservableType extends Observable<any>, InitialValue>(
|
|
28
|
+
observable: ObservableType,
|
|
29
|
+
initialValue: InitialValue,
|
|
30
|
+
): InitialValue | ObservedValueOf<ObservableType>
|
|
31
|
+
/** @public */
|
|
32
|
+
export function useObservable<ObservableType extends Observable<any>, InitialValue>(
|
|
33
|
+
observable: ObservableType,
|
|
34
|
+
initialValue?: InitialValue | (() => InitialValue),
|
|
35
|
+
): InitialValue | ObservedValueOf<ObservableType> {
|
|
21
36
|
/**
|
|
22
37
|
* Store the initialValue in a ref, as we don't want a changed `initialValue` to trigger a re-subscription.
|
|
23
38
|
* But we also don't want the initialValue to be stale if the observable changes.
|
|
24
39
|
*/
|
|
25
|
-
const initialValueRef = useRef(getValue(initialValue) as
|
|
40
|
+
const initialValueRef = useRef(getValue(initialValue) as ObservedValueOf<ObservableType>)
|
|
26
41
|
|
|
27
42
|
/**
|
|
28
43
|
* Ensures that the initialValue is always up-to-date in case the observable changes.
|
|
29
44
|
*/
|
|
30
45
|
useEffect(() => {
|
|
31
|
-
initialValueRef.current = getValue(initialValue) as
|
|
46
|
+
initialValueRef.current = getValue(initialValue) as ObservedValueOf<ObservableType>
|
|
32
47
|
}, [initialValue])
|
|
33
48
|
|
|
34
49
|
const store = useMemo(() => {
|
|
35
50
|
if (!cache.has(observable)) {
|
|
36
|
-
const entry: Partial<CacheRecord<
|
|
51
|
+
const entry: Partial<CacheRecord<ObservedValueOf<ObservableType>>> = {
|
|
37
52
|
snapshot: initialValueRef.current,
|
|
38
53
|
}
|
|
39
54
|
entry.observable = observable.pipe(
|
|
40
55
|
shareReplay({refCount: true, bufferSize: 1}),
|
|
41
|
-
tap(
|
|
56
|
+
tap({
|
|
57
|
+
next: (value) => {
|
|
58
|
+
entry.snapshot = value
|
|
59
|
+
entry.error = undefined
|
|
60
|
+
},
|
|
61
|
+
error: (error: unknown) => (entry.error = error),
|
|
62
|
+
}),
|
|
42
63
|
)
|
|
43
64
|
|
|
44
65
|
// Eagerly subscribe to sync set `entry.currentValue` to what the observable returns, and keep the observable alive until the component unmounts.
|
|
45
66
|
entry.subscription = entry.observable.subscribe()
|
|
46
67
|
|
|
47
|
-
cache.set(observable, entry as CacheRecord<
|
|
68
|
+
cache.set(observable, entry as CacheRecord<ObservedValueOf<ObservableType>>)
|
|
48
69
|
}
|
|
49
70
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
50
71
|
const instance = cache.get(observable)!
|
|
@@ -54,16 +75,27 @@ export function useObservable<ObservableType extends Observable<any>>(
|
|
|
54
75
|
|
|
55
76
|
return {
|
|
56
77
|
subscribe: (onStoreChange: () => void) => {
|
|
57
|
-
const subscription = instance.observable.subscribe(
|
|
78
|
+
const subscription = instance.observable.subscribe({
|
|
79
|
+
next: onStoreChange,
|
|
80
|
+
error: onStoreChange,
|
|
81
|
+
})
|
|
58
82
|
instance.subscription.unsubscribe()
|
|
59
|
-
return () =>
|
|
83
|
+
return () => {
|
|
84
|
+
subscription.unsubscribe()
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
getSnapshot: () => {
|
|
88
|
+
if (instance.error) {
|
|
89
|
+
throw instance.error
|
|
90
|
+
}
|
|
91
|
+
return instance.snapshot
|
|
60
92
|
},
|
|
61
|
-
getSnapshot: () => instance.snapshot,
|
|
62
93
|
}
|
|
63
94
|
}, [observable])
|
|
64
95
|
|
|
65
|
-
return useSyncExternalStore<
|
|
96
|
+
return useSyncExternalStore<ObservedValueOf<ObservableType>>(
|
|
97
|
+
store.subscribe,
|
|
98
|
+
store.getSnapshot,
|
|
99
|
+
typeof initialValueRef.current === 'undefined' ? undefined : () => initialValueRef.current,
|
|
100
|
+
)
|
|
66
101
|
}
|
|
67
|
-
|
|
68
|
-
/** @internal */
|
|
69
|
-
export type UnboxObservable<T> = T extends Observable<infer U> ? U : never
|
|
@@ -19,7 +19,9 @@ export function useObservableEvent<T, U>(
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* This is a ponyfill of the upcoming `useEffectEvent` hook that'll arrive in React 19
|
|
22
|
+
* This is a ponyfill of the upcoming `useEffectEvent` hook that'll arrive in React 19.
|
|
23
|
+
* https://19.react.dev/learn/separating-events-from-effects#declaring-an-effect-event
|
|
24
|
+
* To learn more about the ponyfill itself, see: https://blog.bitsrc.io/a-look-inside-the-useevent-polyfill-from-the-new-react-docs-d1c4739e8072
|
|
23
25
|
*/
|
|
24
26
|
function useEffectEvent<
|
|
25
27
|
const T extends (
|
package/dist/index.d.mts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import {Observable} from 'rxjs'
|
|
2
|
-
|
|
3
|
-
/** @internal */
|
|
4
|
-
export declare type UnboxObservable<T> = T extends Observable<infer U> ? U : never
|
|
5
|
-
|
|
6
|
-
/** @public */
|
|
7
|
-
export declare function useObservable<ObservableType extends Observable<any>>(
|
|
8
|
-
observable: ObservableType,
|
|
9
|
-
initialValue?: UnboxObservable<ObservableType> | (() => UnboxObservable<ObservableType>),
|
|
10
|
-
): UnboxObservable<ObservableType>
|
|
11
|
-
|
|
12
|
-
/** @public */
|
|
13
|
-
export declare function useObservableEvent<T, U>(
|
|
14
|
-
fn: (arg: Observable<T>) => Observable<U>,
|
|
15
|
-
): (arg: T) => void
|
|
16
|
-
|
|
17
|
-
export {}
|
package/dist/index.mjs
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { useRef, useEffect, useMemo, useSyncExternalStore, useState, useInsertionEffect, useCallback } from "react";
|
|
2
|
-
import { shareReplay, tap } from "rxjs/operators";
|
|
3
|
-
import { observableCallback } from "observable-callback";
|
|
4
|
-
function getValue(value) {
|
|
5
|
-
return typeof value == "function" ? value() : value;
|
|
6
|
-
}
|
|
7
|
-
const cache = /* @__PURE__ */ new WeakMap();
|
|
8
|
-
function useObservable(observable, initialValue) {
|
|
9
|
-
const initialValueRef = useRef(getValue(initialValue));
|
|
10
|
-
useEffect(() => {
|
|
11
|
-
initialValueRef.current = getValue(initialValue);
|
|
12
|
-
}, [initialValue]);
|
|
13
|
-
const store = useMemo(() => {
|
|
14
|
-
if (!cache.has(observable)) {
|
|
15
|
-
const entry = {
|
|
16
|
-
snapshot: initialValueRef.current
|
|
17
|
-
};
|
|
18
|
-
entry.observable = observable.pipe(
|
|
19
|
-
shareReplay({ refCount: !0, bufferSize: 1 }),
|
|
20
|
-
tap((value) => entry.snapshot = value)
|
|
21
|
-
), entry.subscription = entry.observable.subscribe(), cache.set(observable, entry);
|
|
22
|
-
}
|
|
23
|
-
const instance = cache.get(observable);
|
|
24
|
-
return instance.subscription.closed && (instance.subscription = instance.observable.subscribe()), {
|
|
25
|
-
subscribe: (onStoreChange) => {
|
|
26
|
-
const subscription = instance.observable.subscribe(onStoreChange);
|
|
27
|
-
return instance.subscription.unsubscribe(), () => subscription.unsubscribe();
|
|
28
|
-
},
|
|
29
|
-
getSnapshot: () => instance.snapshot
|
|
30
|
-
};
|
|
31
|
-
}, [observable]);
|
|
32
|
-
return useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
33
|
-
}
|
|
34
|
-
function useObservableEvent(fn) {
|
|
35
|
-
const [[calls$, call]] = useState(() => observableCallback()), callback = useEffectEvent(fn);
|
|
36
|
-
return useEffect(() => {
|
|
37
|
-
const subscription = calls$.pipe(callback).subscribe();
|
|
38
|
-
return () => subscription.unsubscribe();
|
|
39
|
-
}, [callback, calls$]), call;
|
|
40
|
-
}
|
|
41
|
-
function useEffectEvent(fn) {
|
|
42
|
-
const ref = useRef(null);
|
|
43
|
-
return useInsertionEffect(() => {
|
|
44
|
-
ref.current = fn;
|
|
45
|
-
}, [fn]), useCallback((...args) => {
|
|
46
|
-
const f = ref.current;
|
|
47
|
-
return f(...args);
|
|
48
|
-
}, []);
|
|
49
|
-
}
|
|
50
|
-
export {
|
|
51
|
-
useObservable,
|
|
52
|
-
useObservableEvent
|
|
53
|
-
};
|
|
54
|
-
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","sources":["../src/useObservable.ts","../src/useObservableEvent.ts"],"sourcesContent":["import {useEffect, useMemo, useRef, useSyncExternalStore} from 'react'\nimport type {Observable, Subscription} from 'rxjs'\nimport {shareReplay, tap} from 'rxjs/operators'\n\nfunction getValue<T>(value: T): T extends () => infer U ? U : T {\n return typeof value === 'function' ? value() : value\n}\ninterface CacheRecord<T> {\n subscription: Subscription\n observable: Observable<T>\n snapshot: T\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?: UnboxObservable<ObservableType> | (() => UnboxObservable<ObservableType>),\n): UnboxObservable<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 UnboxObservable<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 UnboxObservable<ObservableType>\n }, [initialValue])\n\n const store = useMemo(() => {\n if (!cache.has(observable)) {\n const entry: Partial<CacheRecord<UnboxObservable<ObservableType>>> = {\n snapshot: initialValueRef.current,\n }\n entry.observable = observable.pipe(\n shareReplay({refCount: true, bufferSize: 1}),\n tap((value) => (entry.snapshot = value)),\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 entry.subscription = entry.observable.subscribe()\n\n cache.set(observable, entry as CacheRecord<UnboxObservable<ObservableType>>)\n }\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const instance = cache.get(observable)!\n if (instance.subscription.closed) {\n instance.subscription = instance.observable.subscribe()\n }\n\n return {\n subscribe: (onStoreChange: () => void) => {\n const subscription = instance.observable.subscribe(onStoreChange)\n instance.subscription.unsubscribe()\n return () => subscription.unsubscribe()\n },\n getSnapshot: () => instance.snapshot,\n }\n }, [observable])\n\n return useSyncExternalStore<UnboxObservable<ObservableType>>(store.subscribe, store.getSnapshot)\n}\n\n/** @internal */\nexport type UnboxObservable<T> = T extends Observable<infer U> ? U : never\n","import {observableCallback} from 'observable-callback'\nimport {useCallback, useEffect, useInsertionEffect, useRef, useState} from 'react'\nimport {type Observable} from 'rxjs'\n\n/** @public */\nexport function useObservableEvent<T, U>(\n fn: (arg: Observable<T>) => Observable<U>,\n): (arg: T) => void {\n const [[calls$, call]] = useState(() => observableCallback<T>())\n\n const callback = useEffectEvent(fn)\n\n useEffect(() => {\n const subscription = calls$.pipe(callback).subscribe()\n return () => subscription.unsubscribe()\n }, [callback, calls$])\n\n return call\n}\n\n/**\n * This is a ponyfill of the upcoming `useEffectEvent` hook that'll arrive in React 19\n */\nfunction useEffectEvent<\n const T extends (\n ...args: // eslint-disable-next-line @typescript-eslint/no-explicit-any\n any[]\n ) => void,\n>(fn: T): T {\n const ref = useRef<T | null>(null)\n useInsertionEffect(() => {\n ref.current = fn\n }, [fn])\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return useCallback((...args: any[]) => {\n const f = ref.current!\n return f(...args)\n }, []) as T\n}\n"],"names":[],"mappings":";;;AAIA,SAAS,SAAY,OAA2C;AAC9D,SAAO,OAAO,SAAU,aAAa,MAAA,IAAU;AACjD;AAOA,MAAM,4BAAY;AAGF,SAAA,cACd,YACA,cACiC;AAKjC,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,YAAY,EAAC,UAAU,IAAM,YAAY,GAAE;AAAA,QAC3C,IAAI,CAAC,UAAW,MAAM,WAAW,KAAM;AAAA,MACzC,GAGA,MAAM,eAAe,MAAM,WAAW,aAEtC,MAAM,IAAI,YAAY,KAAqD;AAAA,IAC7E;AAEM,UAAA,WAAW,MAAM,IAAI,UAAU;AACjC,WAAA,SAAS,aAAa,WACxB,SAAS,eAAe,SAAS,WAAW,cAGvC;AAAA,MACL,WAAW,CAAC,kBAA8B;AACxC,cAAM,eAAe,SAAS,WAAW,UAAU,aAAa;AAChE,eAAA,SAAS,aAAa,YACf,GAAA,MAAM,aAAa;MAC5B;AAAA,MACA,aAAa,MAAM,SAAS;AAAA,IAAA;AAAA,EAC9B,GACC,CAAC,UAAU,CAAC;AAEf,SAAO,qBAAsD,MAAM,WAAW,MAAM,WAAW;AACjG;AC5DO,SAAS,mBACd,IACkB;AAClB,QAAM,CAAC,CAAC,QAAQ,IAAI,CAAC,IAAI,SAAS,MAAM,mBAAsB,CAAC,GAEzD,WAAW,eAAe,EAAE;AAElC,SAAA,UAAU,MAAM;AACd,UAAM,eAAe,OAAO,KAAK,QAAQ,EAAE,UAAU;AAC9C,WAAA,MAAM,aAAa;EACzB,GAAA,CAAC,UAAU,MAAM,CAAC,GAEd;AACT;AAKA,SAAS,eAKP,IAAU;AACJ,QAAA,MAAM,OAAiB,IAAI;AACjC,SAAA,mBAAmB,MAAM;AACvB,QAAI,UAAU;AAAA,KACb,CAAC,EAAE,CAAC,GAEA,YAAY,IAAI,SAAgB;AACrC,UAAM,IAAI,IAAI;AACP,WAAA,EAAE,GAAG,IAAI;AAAA,EAClB,GAAG,CAAE,CAAA;AACP;"}
|