react-rx 3.0.0-7 → 3.0.0-8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: !0 });
3
- var react = require("react"), operators = require("rxjs/operators"), observableCallback = require("observable-callback");
3
+ var react = require("react"), rxjs = require("rxjs"), operators = require("rxjs/operators"), observableCallback = require("observable-callback");
4
4
  function getValue(value) {
5
5
  return typeof value == "function" ? value() : value;
6
6
  }
@@ -16,22 +16,24 @@ function useObservable(observable, initialValue) {
16
16
  snapshot: initialValueRef.current
17
17
  };
18
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
- })
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()
26
31
  ), entry.subscription = entry.observable.subscribe(), cache.set(observable, entry);
27
32
  }
28
33
  const instance = cache.get(observable);
29
34
  return instance.subscription.closed && (instance.subscription = instance.observable.subscribe()), {
30
35
  subscribe: (onStoreChange) => {
31
- const subscription = instance.observable.subscribe({
32
- next: onStoreChange,
33
- error: onStoreChange
34
- });
36
+ const subscription = instance.observable.subscribe(onStoreChange);
35
37
  return instance.subscription.unsubscribe(), () => {
36
38
  subscription.unsubscribe();
37
39
  };
@@ -1 +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;;;"}
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 type Subscription,\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 subscription: Subscription\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,\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(),\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(onStoreChange)\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","map","catchError","of","tap","finalize","share","useSyncExternalStore","useState","observableCallback","useInsertionEffect","useCallback"],"mappings":";;;AAYA,SAAS,SAAY,OAA2C;AAC9D,SAAO,OAAO,SAAU,aAAa,MAAA,IAAU;AACjD;AASA,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,WAAM;AAAA,MACR,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;AACvD,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;ACzGO,SAAS,mBACd,IACkB;AAClB,QAAM,CAAC,CAAC,QAAQ,IAAI,CAAC,IAAIC,MAAS,SAAA,MAAMC,mBAAsB,mBAAA,CAAC,GAEzD,WAAW,eAAe,EAAE;AAElC,SAAAV,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,SAAAY,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.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { useRef, useEffect, useMemo, useSyncExternalStore, useState, useInsertionEffect, useCallback } from "react";
2
- import { shareReplay, tap } from "rxjs/operators";
2
+ import { catchError, of, finalize, share } from "rxjs";
3
+ import { map, tap } from "rxjs/operators";
3
4
  import { observableCallback } from "observable-callback";
4
5
  function getValue(value) {
5
6
  return typeof value == "function" ? value() : value;
@@ -16,22 +17,24 @@ function useObservable(observable, initialValue) {
16
17
  snapshot: initialValueRef.current
17
18
  };
18
19
  entry.observable = observable.pipe(
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
- })
20
+ map((value) => ({ snapshot: value, error: void 0 })),
21
+ catchError((error) => of({ snapshot: void 0, error })),
22
+ tap(({ snapshot, error }) => {
23
+ entry.snapshot = snapshot, entry.error = error;
24
+ }),
25
+ // Note: any value or error emitted by the provided observable will be mapped to the cache entry's mutable state
26
+ // and the observable is thereafter only used as a notifier to call `onStoreChange`, hence the `void` return type.
27
+ map((value) => {
28
+ }),
29
+ // Ensure that the cache entry is deleted when the observable completes or errors.
30
+ finalize(() => cache.delete(observable)),
31
+ share()
26
32
  ), entry.subscription = entry.observable.subscribe(), cache.set(observable, entry);
27
33
  }
28
34
  const instance = cache.get(observable);
29
35
  return instance.subscription.closed && (instance.subscription = instance.observable.subscribe()), {
30
36
  subscribe: (onStoreChange) => {
31
- const subscription = instance.observable.subscribe({
32
- next: onStoreChange,
33
- error: onStoreChange
34
- });
37
+ const subscription = instance.observable.subscribe(onStoreChange);
35
38
  return instance.subscription.unsubscribe(), () => {
36
39
  subscription.unsubscribe();
37
40
  };
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 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;"}
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 type Subscription,\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 subscription: Subscription\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,\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(),\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(onStoreChange)\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":";;;;AAYA,SAAS,SAAY,OAA2C;AAC9D,SAAO,OAAO,SAAU,aAAa,MAAA,IAAU;AACjD;AASA,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;AAAA,MACR,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;AACvD,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;ACzGO,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-7",
3
+ "version": "3.0.0-8",
4
4
  "description": "React + RxJS = <3",
5
5
  "keywords": [
6
6
  "action",
@@ -1,4 +1,4 @@
1
- import {act, render} from '@testing-library/react'
1
+ import {render} from '@testing-library/react'
2
2
  import React from 'react'
3
3
  import {mergeMap, of, Subject, throwError} from 'rxjs'
4
4
  import {expect, test, vitest} from 'vitest'
@@ -28,10 +28,8 @@ test('errors emitted by the observable should be thrown during the react render
28
28
  </div>
29
29
  `)
30
30
 
31
- act(() => {
32
- // Note that the error is thrown later, during the render phase
33
- subject.next({error: true, message: 'Boom'})
34
- })
31
+ // Note that the error is thrown later, during the render phase
32
+ subject.next({error: true, message: 'Boom'})
35
33
 
36
34
  const consoleErrorSpy = vitest.spyOn(globalThis.console, 'error').mockImplementation(() => {
37
35
  // silence console.error()'s
@@ -1,6 +1,6 @@
1
1
  import {act, render, renderHook} from '@testing-library/react'
2
2
  import {createElement, Fragment} from 'react'
3
- import {asyncScheduler, Observable, of, scheduled, Subject, timer} from 'rxjs'
3
+ import {asyncScheduler, Observable, of, ReplaySubject, scheduled, share, Subject, timer} from 'rxjs'
4
4
  import {map} from 'rxjs/operators'
5
5
  import {expect, test} from 'vitest'
6
6
 
@@ -103,6 +103,90 @@ test('should have passed initialValue as initial value from delayed observables'
103
103
  unmount()
104
104
  })
105
105
 
106
+ test('should rerender with initial value if component unmounts and then remounts', () => {
107
+ const values$ = new Subject<string>()
108
+ const firstHook = renderHook(() => useObservable(values$, 'initial'))
109
+
110
+ expect(firstHook.result.current).toBe('initial')
111
+
112
+ act(() => values$.next('something'))
113
+ expect(firstHook.result.current).toBe('something')
114
+
115
+ firstHook.unmount()
116
+
117
+ const nextHook = renderHook(() => useObservable(values$, 'initial2'))
118
+
119
+ expect(nextHook.result.current).toBe('initial2')
120
+ })
121
+
122
+ test('should share the observable between each concurrent subscribing hook', () => {
123
+ let subscribeCount = 0
124
+ const observable = new Observable<number>((subscriber) => {
125
+ subscriber.next(subscribeCount++)
126
+ })
127
+ const firstHook = renderHook(() => useObservable(observable))
128
+ expect(firstHook.result.current).toBe(0)
129
+ const secondHook = renderHook(() => useObservable(observable))
130
+ expect(secondHook.result.current).toBe(0)
131
+ firstHook.unmount()
132
+ secondHook.unmount()
133
+
134
+ const thirdHook = renderHook(() => useObservable(observable))
135
+ expect(thirdHook.result.current).toBe(1)
136
+ thirdHook.unmount()
137
+ })
138
+
139
+ test('should restart any completed observable on mount', () => {
140
+ let subscribeCount = 0
141
+ let unsubscribeCount = 0
142
+
143
+ type Notification<T> =
144
+ | {kind: 'next'; value: T}
145
+ | {kind: 'error'; error: Error}
146
+ | {kind: 'complete'}
147
+
148
+ const notifications$ = new Subject<Notification<string>>()
149
+
150
+ const observable = new Observable<string>((subscriber) => {
151
+ subscribeCount++
152
+ const subscription = notifications$.subscribe((notification) => {
153
+ if (notification.kind === 'next') {
154
+ subscriber.next(notification.value)
155
+ } else if (notification.kind === 'error') {
156
+ subscriber.error(notification.error)
157
+ } else if (notification.kind === 'complete') {
158
+ subscriber.complete()
159
+ }
160
+ })
161
+ return () => {
162
+ unsubscribeCount++
163
+ subscription.unsubscribe()
164
+ }
165
+ }).pipe(share({connector: () => new ReplaySubject(1)}))
166
+
167
+ const firstHook = renderHook(() => useObservable(observable, 'initial'))
168
+ expect(firstHook.result.current).toBe('initial')
169
+
170
+ act(() => notifications$.next({kind: 'next', value: 'something'}))
171
+ expect(firstHook.result.current).toBe('something')
172
+ act(() => notifications$.next({kind: 'complete'}))
173
+ expect(firstHook.result.current).toBe('something')
174
+ act(() => notifications$.next({kind: 'next', value: 'after complete'}))
175
+ expect(firstHook.result.current).toBe('something')
176
+
177
+ expect(subscribeCount).toBe(1)
178
+ expect(unsubscribeCount).toBe(1)
179
+
180
+ firstHook.unmount()
181
+
182
+ const secondHook = renderHook(() => useObservable(observable))
183
+ expect(secondHook.result.current).toBe(undefined)
184
+ expect(subscribeCount).toBe(2)
185
+ expect(unsubscribeCount).toBe(1)
186
+ secondHook.unmount()
187
+ expect(unsubscribeCount).toBe(2)
188
+ })
189
+
106
190
  test('should update with values from observables', () => {
107
191
  const values$ = new Subject<string>()
108
192
  const {result, unmount} = renderHook(() => useObservable(values$))
@@ -1,13 +1,22 @@
1
1
  import {useEffect, useMemo, useRef, useSyncExternalStore} from 'react'
2
- import {type Observable, type ObservedValueOf, type Subscription} from 'rxjs'
3
- import {shareReplay, tap} from 'rxjs/operators'
2
+ import {
3
+ catchError,
4
+ finalize,
5
+ type Observable,
6
+ type ObservedValueOf,
7
+ of,
8
+ share,
9
+ type Subscription,
10
+ } from 'rxjs'
11
+ import {map, tap} from 'rxjs/operators'
4
12
 
5
13
  function getValue<T>(value: T): T extends () => infer U ? U : T {
6
14
  return typeof value === 'function' ? value() : value
7
15
  }
16
+
8
17
  interface CacheRecord<T> {
9
18
  subscription: Subscription
10
- observable: Observable<T>
19
+ observable: Observable<void>
11
20
  snapshot: T
12
21
  error?: unknown
13
22
  }
@@ -52,14 +61,18 @@ export function useObservable<ObservableType extends Observable<any>, InitialVal
52
61
  snapshot: initialValueRef.current,
53
62
  }
54
63
  entry.observable = observable.pipe(
55
- shareReplay({refCount: true, bufferSize: 1}),
56
- tap({
57
- next: (value) => {
58
- entry.snapshot = value
59
- entry.error = undefined
60
- },
61
- error: (error: unknown) => (entry.error = error),
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
62
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(),
63
76
  )
64
77
 
65
78
  // Eagerly subscribe to sync set `entry.currentValue` to what the observable returns, and keep the observable alive until the component unmounts.
@@ -75,10 +88,7 @@ export function useObservable<ObservableType extends Observable<any>, InitialVal
75
88
 
76
89
  return {
77
90
  subscribe: (onStoreChange: () => void) => {
78
- const subscription = instance.observable.subscribe({
79
- next: onStoreChange,
80
- error: onStoreChange,
81
- })
91
+ const subscription = instance.observable.subscribe(onStoreChange)
82
92
  instance.subscription.unsubscribe()
83
93
  return () => {
84
94
  subscription.unsubscribe()