react-rx 2.0.3 → 2.1.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/.idea/vcs.xml CHANGED
@@ -1,9 +1,6 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
2
  <project version="4">
3
3
  <component name="VcsDirectoryMappings">
4
- <mapping directory="$PROJECT_DIR$/../../_/parcel-transformer-mdx-with-toc" vcs="Git" />
5
- <mapping directory="$PROJECT_DIR$/../react-repl" vcs="Git" />
6
4
  <mapping directory="$PROJECT_DIR$" vcs="Git" />
7
- <mapping directory="$PROJECT_DIR$/../react-rx.dev" vcs="Git" />
8
5
  </component>
9
6
  </project>
@@ -1,7 +1,7 @@
1
1
  import { Observable } from 'rxjs';
2
2
  import { FunctionComponent, ReactNode, Ref } from 'react';
3
3
  declare type Component<Props> = (input$: Observable<Props>) => Observable<ReactNode>;
4
- export declare function reactiveComponent<Props>(observable: Observable<Props>): FunctionComponent<{}>;
4
+ export declare function reactiveComponent<Props>(observable: Observable<Props>): FunctionComponent<Props>;
5
5
  export declare function reactiveComponent<Props>(component: Component<Props>): FunctionComponent<Props>;
6
6
  declare type ForwardRefComponent<RefType, Props> = (input$: Observable<Props>, ref: Ref<RefType>) => Observable<ReactNode>;
7
7
  export declare function forwardRef<RefType, Props = {}>(component: ForwardRefComponent<RefType, Props>): import("react").ForwardRefExoticComponent<import("react").PropsWithoutRef<Props> & import("react").RefAttributes<RefType>>;
@@ -3,19 +3,43 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.useAsObservable = void 0;
4
4
  var rxjs_1 = require("rxjs");
5
5
  var react_1 = require("react");
6
- var useIsomorphicEffect_1 = require("./useIsomorphicEffect");
6
+ var operators_1 = require("rxjs/operators");
7
7
  function useAsObservable(value, operator) {
8
- var _a = (0, react_1.useMemo)(function () {
8
+ var setup = (0, react_1.useCallback)(function () {
9
9
  var subject = new rxjs_1.BehaviorSubject(value);
10
- var observable = subject.asObservable();
10
+ var observable = subject.asObservable().pipe((0, operators_1.distinctUntilChanged)());
11
11
  return [operator ? observable.pipe(operator) : observable, subject];
12
- }, []), observable = _a[0], subject = _a[1];
13
- (0, useIsomorphicEffect_1.useIsomorphicEffect)(function () {
12
+ }, []);
13
+ var ref = (0, react_1.useRef)();
14
+ if (!ref.current) {
15
+ ref.current = setup();
16
+ }
17
+ var observable = ref.current[0];
18
+ (0, react_1.useEffect)(function () {
19
+ if (!ref.current) {
20
+ return;
21
+ }
22
+ var _a = ref.current, subject = _a[1];
14
23
  subject.next(value);
15
- }, [value]);
24
+ }, [value, ref]);
25
+ var shouldRestoreSubscriptionRef = (0, react_1.useRef)(false);
16
26
  (0, react_1.useEffect)(function () {
27
+ if (shouldRestoreSubscriptionRef.current) {
28
+ if (!ref.current) {
29
+ ref.current = setup();
30
+ }
31
+ shouldRestoreSubscriptionRef.current = false;
32
+ }
17
33
  return function () {
34
+ if (!ref.current) {
35
+ return;
36
+ }
37
+ // React StrictMode will call effects as `setup + teardown + setup` thus we can't trust this callback as "react is about to unmount"
38
+ // Tracking this ref lets us set the subscription back up on the next `setup` call if needed, and if it really did unmounted then all is well
39
+ shouldRestoreSubscriptionRef.current = true;
40
+ var _a = ref.current, subject = _a[1];
18
41
  subject.complete();
42
+ ref.current = undefined;
19
43
  };
20
44
  }, []);
21
45
  return observable;
@@ -1,2 +1,2 @@
1
- import { useLayoutEffect } from 'react';
2
- export declare const useIsomorphicEffect: typeof useLayoutEffect;
1
+ import { useEffect } from 'react';
2
+ export declare const useIsomorphicEffect: typeof useEffect;
@@ -1,31 +1,59 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.useMemoObservable = exports.useObservable = void 0;
4
- var rxjs_1 = require("rxjs");
5
4
  var react_1 = require("react");
6
5
  var shim_1 = require("use-sync-external-store/shim");
6
+ var operators_1 = require("rxjs/operators");
7
7
  function getValue(value) {
8
8
  return typeof value === 'function' ? value() : value;
9
9
  }
10
+ var cache = new WeakMap();
11
+ function getOrCreateStore(inputObservable, initialValue) {
12
+ if (!cache.has(inputObservable)) {
13
+ var entry_1 = { currentValue: initialValue };
14
+ entry_1.observable = inputObservable.pipe((0, operators_1.shareReplay)({ refCount: true, bufferSize: 1 }), (0, operators_1.tap)(function (value) { return (entry_1.currentValue = value); }));
15
+ // Eagerly subscribe to sync set `entry.currentValue` to what the observable returns
16
+ entry_1.subscription = entry_1.observable.subscribe();
17
+ cache.set(inputObservable, entry_1);
18
+ }
19
+ return cache.get(inputObservable);
20
+ }
10
21
  function useObservable(observable, initialValue) {
11
22
  var _a = (0, react_1.useMemo)(function () {
12
- var currentValue = getValue(initialValue);
13
- var subject = new rxjs_1.Subject();
14
- var subscription = observable.subscribe(function (value) {
15
- currentValue = value;
16
- subject.next(value);
17
- });
23
+ var store = getOrCreateStore(observable, getValue(initialValue));
24
+ if (store.subscription.closed) {
25
+ store.subscription = store.observable.subscribe();
26
+ }
18
27
  return [
19
- function () { return currentValue; },
20
- function (callback) {
21
- var subjectSub = subject.subscribe(function (value) { return callback(value); });
28
+ function getSnapshot() {
29
+ // @TODO: perf opt opportunity: we could do `store.subscription.unsubscribe()` here to clear up some memory, as this subscription is only needed to provide a sync initialValue.
30
+ return store.currentValue;
31
+ },
32
+ function subscribe(callback) {
33
+ // @TODO: perf opt opportunity: we could do `store.subscription.unsubscribe()` here as we only need 1 subscription active to keep the observer alive
34
+ var sub = store.observable.subscribe(callback);
22
35
  return function () {
23
- subscription.unsubscribe();
24
- subjectSub.unsubscribe();
36
+ sub.unsubscribe();
25
37
  };
26
38
  },
27
39
  ];
28
40
  }, [observable]), getSnapshot = _a[0], subscribe = _a[1];
41
+ var shouldRestoreSubscriptionRef = (0, react_1.useRef)(false);
42
+ (0, react_1.useEffect)(function () {
43
+ var store = getOrCreateStore(observable, getValue(initialValue));
44
+ if (shouldRestoreSubscriptionRef.current) {
45
+ if (store.subscription.closed) {
46
+ store.subscription = store.observable.subscribe();
47
+ }
48
+ shouldRestoreSubscriptionRef.current = false;
49
+ }
50
+ return function () {
51
+ // React StrictMode will call effects as `setup + teardown + setup` thus we can't trust this callback as "react is about to unmount"
52
+ // Tracking this ref lets us set the subscription back up on the next `setup` call if needed, and if it really did unmounted then all is well
53
+ shouldRestoreSubscriptionRef.current = !store.subscription.closed;
54
+ store.subscription.unsubscribe();
55
+ };
56
+ }, [observable]);
29
57
  return (0, shim_1.useSyncExternalStore)(subscribe, getSnapshot);
30
58
  }
31
59
  exports.useObservable = useObservable;
@@ -1,7 +1,7 @@
1
1
  import { Observable } from 'rxjs';
2
2
  import { FunctionComponent, ReactNode, Ref } from 'react';
3
3
  declare type Component<Props> = (input$: Observable<Props>) => Observable<ReactNode>;
4
- export declare function reactiveComponent<Props>(observable: Observable<Props>): FunctionComponent<{}>;
4
+ export declare function reactiveComponent<Props>(observable: Observable<Props>): FunctionComponent<Props>;
5
5
  export declare function reactiveComponent<Props>(component: Component<Props>): FunctionComponent<Props>;
6
6
  declare type ForwardRefComponent<RefType, Props> = (input$: Observable<Props>, ref: Ref<RefType>) => Observable<ReactNode>;
7
7
  export declare function forwardRef<RefType, Props = {}>(component: ForwardRefComponent<RefType, Props>): import("react").ForwardRefExoticComponent<import("react").PropsWithoutRef<Props> & import("react").RefAttributes<RefType>>;
@@ -1,18 +1,42 @@
1
1
  import { BehaviorSubject } from 'rxjs';
2
- import { useEffect, useMemo } from 'react';
3
- import { useIsomorphicEffect } from './useIsomorphicEffect';
2
+ import { useCallback, useEffect, useRef } from 'react';
3
+ import { distinctUntilChanged } from 'rxjs/operators';
4
4
  export function useAsObservable(value, operator) {
5
- const [observable, subject] = useMemo(() => {
5
+ const setup = useCallback(() => {
6
6
  const subject = new BehaviorSubject(value);
7
- const observable = subject.asObservable();
7
+ const observable = subject.asObservable().pipe(distinctUntilChanged());
8
8
  return [operator ? observable.pipe(operator) : observable, subject];
9
9
  }, []);
10
- useIsomorphicEffect(() => {
10
+ const ref = useRef();
11
+ if (!ref.current) {
12
+ ref.current = setup();
13
+ }
14
+ const [observable] = ref.current;
15
+ useEffect(() => {
16
+ if (!ref.current) {
17
+ return;
18
+ }
19
+ const [, subject] = ref.current;
11
20
  subject.next(value);
12
- }, [value]);
21
+ }, [value, ref]);
22
+ const shouldRestoreSubscriptionRef = useRef(false);
13
23
  useEffect(() => {
24
+ if (shouldRestoreSubscriptionRef.current) {
25
+ if (!ref.current) {
26
+ ref.current = setup();
27
+ }
28
+ shouldRestoreSubscriptionRef.current = false;
29
+ }
14
30
  return () => {
31
+ if (!ref.current) {
32
+ return;
33
+ }
34
+ // React StrictMode will call effects as `setup + teardown + setup` thus we can't trust this callback as "react is about to unmount"
35
+ // Tracking this ref lets us set the subscription back up on the next `setup` call if needed, and if it really did unmounted then all is well
36
+ shouldRestoreSubscriptionRef.current = true;
37
+ const [, subject] = ref.current;
15
38
  subject.complete();
39
+ ref.current = undefined;
16
40
  };
17
41
  }, []);
18
42
  return observable;
@@ -1,2 +1,2 @@
1
- import { useLayoutEffect } from 'react';
2
- export declare const useIsomorphicEffect: typeof useLayoutEffect;
1
+ import { useEffect } from 'react';
2
+ export declare const useIsomorphicEffect: typeof useEffect;
@@ -1,28 +1,56 @@
1
- import { Subject } from 'rxjs';
2
- import { useMemo } from 'react';
1
+ import { useEffect, useMemo, useRef } from 'react';
3
2
  import { useSyncExternalStore } from 'use-sync-external-store/shim';
3
+ import { shareReplay, tap } from 'rxjs/operators';
4
4
  function getValue(value) {
5
5
  return typeof value === 'function' ? value() : value;
6
6
  }
7
+ const cache = new WeakMap();
8
+ function getOrCreateStore(inputObservable, initialValue) {
9
+ if (!cache.has(inputObservable)) {
10
+ const entry = { currentValue: initialValue };
11
+ entry.observable = inputObservable.pipe(shareReplay({ refCount: true, bufferSize: 1 }), tap(value => (entry.currentValue = value)));
12
+ // Eagerly subscribe to sync set `entry.currentValue` to what the observable returns
13
+ entry.subscription = entry.observable.subscribe();
14
+ cache.set(inputObservable, entry);
15
+ }
16
+ return cache.get(inputObservable);
17
+ }
7
18
  export function useObservable(observable, initialValue) {
8
19
  const [getSnapshot, subscribe] = useMemo(() => {
9
- let currentValue = getValue(initialValue);
10
- const subject = new Subject();
11
- const subscription = observable.subscribe(value => {
12
- currentValue = value;
13
- subject.next(value);
14
- });
20
+ const store = getOrCreateStore(observable, getValue(initialValue));
21
+ if (store.subscription.closed) {
22
+ store.subscription = store.observable.subscribe();
23
+ }
15
24
  return [
16
- () => currentValue,
17
- (callback) => {
18
- const subjectSub = subject.subscribe(value => callback(value));
25
+ function getSnapshot() {
26
+ // @TODO: perf opt opportunity: we could do `store.subscription.unsubscribe()` here to clear up some memory, as this subscription is only needed to provide a sync initialValue.
27
+ return store.currentValue;
28
+ },
29
+ function subscribe(callback) {
30
+ // @TODO: perf opt opportunity: we could do `store.subscription.unsubscribe()` here as we only need 1 subscription active to keep the observer alive
31
+ const sub = store.observable.subscribe(callback);
19
32
  return () => {
20
- subscription.unsubscribe();
21
- subjectSub.unsubscribe();
33
+ sub.unsubscribe();
22
34
  };
23
35
  },
24
36
  ];
25
37
  }, [observable]);
38
+ const shouldRestoreSubscriptionRef = useRef(false);
39
+ useEffect(() => {
40
+ const store = getOrCreateStore(observable, getValue(initialValue));
41
+ if (shouldRestoreSubscriptionRef.current) {
42
+ if (store.subscription.closed) {
43
+ store.subscription = store.observable.subscribe();
44
+ }
45
+ shouldRestoreSubscriptionRef.current = false;
46
+ }
47
+ return () => {
48
+ // React StrictMode will call effects as `setup + teardown + setup` thus we can't trust this callback as "react is about to unmount"
49
+ // Tracking this ref lets us set the subscription back up on the next `setup` call if needed, and if it really did unmounted then all is well
50
+ shouldRestoreSubscriptionRef.current = !store.subscription.closed;
51
+ store.subscription.unsubscribe();
52
+ };
53
+ }, [observable]);
26
54
  return useSyncExternalStore(subscribe, getSnapshot);
27
55
  }
28
56
  export function useMemoObservable(observableOrFactory, deps, initialValue) {
@@ -1,7 +1,7 @@
1
1
  import { Observable } from 'rxjs';
2
2
  import { FunctionComponent, ReactNode, Ref } from 'react';
3
3
  declare type Component<Props> = (input$: Observable<Props>) => Observable<ReactNode>;
4
- export declare function reactiveComponent<Props>(observable: Observable<Props>): FunctionComponent<{}>;
4
+ export declare function reactiveComponent<Props>(observable: Observable<Props>): FunctionComponent<Props>;
5
5
  export declare function reactiveComponent<Props>(component: Component<Props>): FunctionComponent<Props>;
6
6
  declare type ForwardRefComponent<RefType, Props> = (input$: Observable<Props>, ref: Ref<RefType>) => Observable<ReactNode>;
7
7
  export declare function forwardRef<RefType, Props = {}>(component: ForwardRefComponent<RefType, Props>): import("react").ForwardRefExoticComponent<import("react").PropsWithoutRef<Props> & import("react").RefAttributes<RefType>>;
@@ -1,18 +1,42 @@
1
1
  import { BehaviorSubject } from 'rxjs';
2
- import { useEffect, useMemo } from 'react';
3
- import { useIsomorphicEffect } from './useIsomorphicEffect';
2
+ import { useCallback, useEffect, useRef } from 'react';
3
+ import { distinctUntilChanged } from 'rxjs/operators';
4
4
  export function useAsObservable(value, operator) {
5
- var _a = useMemo(function () {
5
+ var setup = useCallback(function () {
6
6
  var subject = new BehaviorSubject(value);
7
- var observable = subject.asObservable();
7
+ var observable = subject.asObservable().pipe(distinctUntilChanged());
8
8
  return [operator ? observable.pipe(operator) : observable, subject];
9
- }, []), observable = _a[0], subject = _a[1];
10
- useIsomorphicEffect(function () {
9
+ }, []);
10
+ var ref = useRef();
11
+ if (!ref.current) {
12
+ ref.current = setup();
13
+ }
14
+ var observable = ref.current[0];
15
+ useEffect(function () {
16
+ if (!ref.current) {
17
+ return;
18
+ }
19
+ var _a = ref.current, subject = _a[1];
11
20
  subject.next(value);
12
- }, [value]);
21
+ }, [value, ref]);
22
+ var shouldRestoreSubscriptionRef = useRef(false);
13
23
  useEffect(function () {
24
+ if (shouldRestoreSubscriptionRef.current) {
25
+ if (!ref.current) {
26
+ ref.current = setup();
27
+ }
28
+ shouldRestoreSubscriptionRef.current = false;
29
+ }
14
30
  return function () {
31
+ if (!ref.current) {
32
+ return;
33
+ }
34
+ // React StrictMode will call effects as `setup + teardown + setup` thus we can't trust this callback as "react is about to unmount"
35
+ // Tracking this ref lets us set the subscription back up on the next `setup` call if needed, and if it really did unmounted then all is well
36
+ shouldRestoreSubscriptionRef.current = true;
37
+ var _a = ref.current, subject = _a[1];
15
38
  subject.complete();
39
+ ref.current = undefined;
16
40
  };
17
41
  }, []);
18
42
  return observable;
@@ -1,2 +1,2 @@
1
- import { useLayoutEffect } from 'react';
2
- export declare const useIsomorphicEffect: typeof useLayoutEffect;
1
+ import { useEffect } from 'react';
2
+ export declare const useIsomorphicEffect: typeof useEffect;
@@ -1,28 +1,56 @@
1
- import { Subject } from 'rxjs';
2
- import { useMemo } from 'react';
1
+ import { useEffect, useMemo, useRef } from 'react';
3
2
  import { useSyncExternalStore } from 'use-sync-external-store/shim';
3
+ import { shareReplay, tap } from 'rxjs/operators';
4
4
  function getValue(value) {
5
5
  return typeof value === 'function' ? value() : value;
6
6
  }
7
+ var cache = new WeakMap();
8
+ function getOrCreateStore(inputObservable, initialValue) {
9
+ if (!cache.has(inputObservable)) {
10
+ var entry_1 = { currentValue: initialValue };
11
+ entry_1.observable = inputObservable.pipe(shareReplay({ refCount: true, bufferSize: 1 }), tap(function (value) { return (entry_1.currentValue = value); }));
12
+ // Eagerly subscribe to sync set `entry.currentValue` to what the observable returns
13
+ entry_1.subscription = entry_1.observable.subscribe();
14
+ cache.set(inputObservable, entry_1);
15
+ }
16
+ return cache.get(inputObservable);
17
+ }
7
18
  export function useObservable(observable, initialValue) {
8
19
  var _a = useMemo(function () {
9
- var currentValue = getValue(initialValue);
10
- var subject = new Subject();
11
- var subscription = observable.subscribe(function (value) {
12
- currentValue = value;
13
- subject.next(value);
14
- });
20
+ var store = getOrCreateStore(observable, getValue(initialValue));
21
+ if (store.subscription.closed) {
22
+ store.subscription = store.observable.subscribe();
23
+ }
15
24
  return [
16
- function () { return currentValue; },
17
- function (callback) {
18
- var subjectSub = subject.subscribe(function (value) { return callback(value); });
25
+ function getSnapshot() {
26
+ // @TODO: perf opt opportunity: we could do `store.subscription.unsubscribe()` here to clear up some memory, as this subscription is only needed to provide a sync initialValue.
27
+ return store.currentValue;
28
+ },
29
+ function subscribe(callback) {
30
+ // @TODO: perf opt opportunity: we could do `store.subscription.unsubscribe()` here as we only need 1 subscription active to keep the observer alive
31
+ var sub = store.observable.subscribe(callback);
19
32
  return function () {
20
- subscription.unsubscribe();
21
- subjectSub.unsubscribe();
33
+ sub.unsubscribe();
22
34
  };
23
35
  },
24
36
  ];
25
37
  }, [observable]), getSnapshot = _a[0], subscribe = _a[1];
38
+ var shouldRestoreSubscriptionRef = useRef(false);
39
+ useEffect(function () {
40
+ var store = getOrCreateStore(observable, getValue(initialValue));
41
+ if (shouldRestoreSubscriptionRef.current) {
42
+ if (store.subscription.closed) {
43
+ store.subscription = store.observable.subscribe();
44
+ }
45
+ shouldRestoreSubscriptionRef.current = false;
46
+ }
47
+ return function () {
48
+ // React StrictMode will call effects as `setup + teardown + setup` thus we can't trust this callback as "react is about to unmount"
49
+ // Tracking this ref lets us set the subscription back up on the next `setup` call if needed, and if it really did unmounted then all is well
50
+ shouldRestoreSubscriptionRef.current = !store.subscription.closed;
51
+ store.subscription.unsubscribe();
52
+ };
53
+ }, [observable]);
26
54
  return useSyncExternalStore(subscribe, getSnapshot);
27
55
  }
28
56
  export function useMemoObservable(observableOrFactory, deps, initialValue) {
package/jest.config.ts ADDED
@@ -0,0 +1,14 @@
1
+ export default {
2
+ preset: 'ts-jest',
3
+ // verbose: true,
4
+ testEnvironment: 'jsdom',
5
+ testEnvironmentOptions: {
6
+ url: 'http://localhost/',
7
+ },
8
+ globals: {
9
+ 'ts-jest': {
10
+ warnOnly: true,
11
+ },
12
+ },
13
+ rootDir: 'src',
14
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-rx",
3
- "version": "2.0.3",
3
+ "version": "2.1.0",
4
4
  "description": "React + RxJS = <3",
5
5
  "main": "dist/cjs/index.js",
6
6
  "types": "dist/cjs/index.d.ts",
@@ -30,12 +30,11 @@
30
30
  "rxjs": "^6.5 || ^7"
31
31
  },
32
32
  "devDependencies": {
33
- "@testing-library/dom": "^7.29.6",
34
- "@testing-library/react": "^11.2.5",
35
- "@testing-library/react-hooks": "^5.0.3",
36
- "@types/jest": "^26.0.20",
37
- "@types/react": "^17.0.2",
38
- "@types/react-dom": "^17.0.1",
33
+ "@testing-library/dom": "^8.14.0",
34
+ "@testing-library/react": "^13.3.0",
35
+ "@types/jest": "^28.1.4",
36
+ "@types/react": "^18.0.12",
37
+ "@types/react-dom": "^18.0.5",
39
38
  "@types/use-sync-external-store": "^0.0.3",
40
39
  "@typescript-eslint/eslint-plugin": "^4.15.2",
41
40
  "@typescript-eslint/parser": "^4.15.2",
@@ -43,16 +42,17 @@
43
42
  "eslint-config-prettier": "^8.1.0",
44
43
  "eslint-plugin-prettier": "^3.3.1",
45
44
  "eslint-plugin-react": "^7.22.0",
46
- "jest": "^26.6.3",
47
- "jsdom": "^16.4.0",
45
+ "jest": "^28.1.2",
46
+ "jest-environment-jsdom": "^28.1.2",
47
+ "jsdom": "^20.0.0",
48
48
  "npm-run-all": "^4.1.5",
49
49
  "prettier": "2.2.1",
50
- "react": "^17.0.1",
51
- "react-dom": "^17.0.1",
52
- "react-test-renderer": "^17.0.1",
50
+ "react": "^18.2.0",
51
+ "react-dom": "^18.2.0",
52
+ "react-test-renderer": "^18.2.0",
53
53
  "rimraf": "^3.0.2",
54
54
  "rxjs": "^6.5.5",
55
- "ts-jest": "^26.5.2",
55
+ "ts-jest": "^28.0.5",
56
56
  "typescript": "^4.2.2"
57
57
  },
58
58
  "repository": {
@@ -26,16 +26,16 @@ function fromComponent<Props>(component: Component<Props>): FunctionComponent<Pr
26
26
  return wrappedComponent
27
27
  }
28
28
 
29
- function fromObservable<Props>(input$: Observable<Props>): FunctionComponent<{}> {
29
+ function fromObservable<Props>(input$: Observable<ReactNode>): FunctionComponent<{}> {
30
30
  return function ReactiveComponent() {
31
31
  return createElement(Fragment, null, useObservable<ReactNode>(input$))
32
32
  }
33
33
  }
34
34
 
35
- export function reactiveComponent<Props>(observable: Observable<Props>): FunctionComponent<{}>
35
+ export function reactiveComponent<Props>(observable: Observable<Props>): FunctionComponent<Props>
36
36
  export function reactiveComponent<Props>(component: Component<Props>): FunctionComponent<Props>
37
37
  export function reactiveComponent<Props>(
38
- observableOrComponent: Observable<Props> | Component<Props>,
38
+ observableOrComponent: Observable<ReactNode> | Component<Props>,
39
39
  ) {
40
40
  return typeof observableOrComponent === 'function'
41
41
  ? fromComponent(observableOrComponent)
@@ -1,6 +1,6 @@
1
1
  import {BehaviorSubject, Observable} from 'rxjs'
2
- import {useEffect, useMemo} from 'react'
3
- import {useIsomorphicEffect} from './useIsomorphicEffect'
2
+ import {useCallback, useEffect, useRef} from 'react'
3
+ import {distinctUntilChanged} from 'rxjs/operators'
4
4
 
5
5
  /**
6
6
  * React hook to convert any props or state value into an observable
@@ -17,22 +17,49 @@ export function useAsObservable<T, K = T>(
17
17
  value: T,
18
18
  operator?: (input: Observable<T>) => Observable<K>,
19
19
  ): Observable<T | K> {
20
- const [observable, subject] = useMemo(() => {
20
+ const setup = useCallback((): [Observable<T | K>, BehaviorSubject<T>] => {
21
21
  const subject = new BehaviorSubject(value)
22
22
 
23
- const observable = subject.asObservable()
23
+ const observable = subject.asObservable().pipe(distinctUntilChanged())
24
24
  return [operator ? observable.pipe(operator) : observable, subject]
25
25
  }, [])
26
26
 
27
- useIsomorphicEffect(() => {
27
+ const ref = useRef<[Observable<T | K>, BehaviorSubject<T>]>()
28
+
29
+ if (!ref.current) {
30
+ ref.current = setup()
31
+ }
32
+
33
+ const [observable] = ref.current
34
+
35
+ useEffect(() => {
36
+ if (!ref.current) {
37
+ return
38
+ }
39
+ const [, subject] = ref.current
28
40
  subject.next(value)
29
- }, [value])
41
+ }, [value, ref])
30
42
 
43
+ const shouldRestoreSubscriptionRef = useRef(false)
31
44
  useEffect(() => {
45
+ if (shouldRestoreSubscriptionRef.current) {
46
+ if (!ref.current) {
47
+ ref.current = setup()
48
+ }
49
+ shouldRestoreSubscriptionRef.current = false
50
+ }
51
+
32
52
  return () => {
53
+ if (!ref.current) {
54
+ return
55
+ }
56
+ // React StrictMode will call effects as `setup + teardown + setup` thus we can't trust this callback as "react is about to unmount"
57
+ // Tracking this ref lets us set the subscription back up on the next `setup` call if needed, and if it really did unmounted then all is well
58
+ shouldRestoreSubscriptionRef.current = true
59
+ const [, subject] = ref.current
33
60
  subject.complete()
61
+ ref.current = undefined
34
62
  }
35
63
  }, [])
36
-
37
64
  return observable
38
65
  }
@@ -1,33 +1,79 @@
1
- import {Observable, Subject, Subscription} from 'rxjs'
2
- import {DependencyList, useMemo, useRef} from 'react'
1
+ import {Observable, Subscription} from 'rxjs'
2
+ import {DependencyList, useEffect, useMemo, useRef} from 'react'
3
3
  import {useSyncExternalStore} from 'use-sync-external-store/shim'
4
+ import {shareReplay, tap} from 'rxjs/operators'
4
5
 
5
6
  function getValue<T>(value: T): T extends () => infer U ? U : T {
6
7
  return typeof value === 'function' ? value() : value
7
8
  }
8
9
 
10
+ interface CacheRecord<T> {
11
+ subscription: Subscription
12
+ observable: Observable<T>
13
+ currentValue: T
14
+ }
15
+
16
+ const cache = new WeakMap<Observable<any>, CacheRecord<any>>()
17
+ function getOrCreateStore<T>(inputObservable: Observable<T>, initialValue: T) {
18
+ if (!cache.has(inputObservable)) {
19
+ const entry: Partial<CacheRecord<T>> = {currentValue: initialValue}
20
+ entry.observable = inputObservable.pipe(
21
+ shareReplay({refCount: true, bufferSize: 1}),
22
+ tap(value => (entry.currentValue = value)),
23
+ )
24
+
25
+ // Eagerly subscribe to sync set `entry.currentValue` to what the observable returns
26
+ entry.subscription = entry.observable.subscribe()
27
+
28
+ cache.set(inputObservable, entry as CacheRecord<T>)
29
+ }
30
+ return cache.get(inputObservable)!
31
+ }
32
+
9
33
  export function useObservable<T>(observable: Observable<T>): T | undefined
10
34
  export function useObservable<T>(observable: Observable<T>, initialValue: T): T
11
35
  export function useObservable<T>(observable: Observable<T>, initialValue: () => T): T
12
36
  export function useObservable<T>(observable: Observable<T>, initialValue?: T | (() => T)) {
13
- const [getSnapshot, subscribe] = useMemo(() => {
14
- let currentValue = getValue(initialValue)
15
- const subject = new Subject<T>()
16
- const subscription = observable.subscribe(value => {
17
- currentValue = value
18
- subject.next(value)
19
- })
37
+ const [getSnapshot, subscribe] = useMemo<
38
+ [() => T, Parameters<typeof useSyncExternalStore>[0]]
39
+ >(() => {
40
+ const store = getOrCreateStore(observable, getValue(initialValue))
41
+ if (store.subscription.closed) {
42
+ store.subscription = store.observable.subscribe()
43
+ }
20
44
  return [
21
- () => currentValue,
22
- (callback: (value: T) => void) => {
23
- const subjectSub = subject.subscribe(value => callback(value))
45
+ function getSnapshot() {
46
+ // @TODO: perf opt opportunity: we could do `store.subscription.unsubscribe()` here to clear up some memory, as this subscription is only needed to provide a sync initialValue.
47
+ return store.currentValue
48
+ },
49
+ function subscribe(callback: () => void) {
50
+ // @TODO: perf opt opportunity: we could do `store.subscription.unsubscribe()` here as we only need 1 subscription active to keep the observer alive
51
+ const sub = store.observable.subscribe(callback)
24
52
  return () => {
25
- subscription.unsubscribe()
26
- subjectSub.unsubscribe()
53
+ sub.unsubscribe()
27
54
  }
28
55
  },
29
56
  ]
30
57
  }, [observable])
58
+
59
+ const shouldRestoreSubscriptionRef = useRef(false)
60
+ useEffect(() => {
61
+ const store = getOrCreateStore(observable, getValue(initialValue))
62
+ if (shouldRestoreSubscriptionRef.current) {
63
+ if (store.subscription.closed) {
64
+ store.subscription = store.observable.subscribe()
65
+ }
66
+ shouldRestoreSubscriptionRef.current = false
67
+ }
68
+
69
+ return () => {
70
+ // React StrictMode will call effects as `setup + teardown + setup` thus we can't trust this callback as "react is about to unmount"
71
+ // Tracking this ref lets us set the subscription back up on the next `setup` call if needed, and if it really did unmounted then all is well
72
+ shouldRestoreSubscriptionRef.current = !store.subscription.closed
73
+ store.subscription.unsubscribe()
74
+ }
75
+ }, [observable])
76
+
31
77
  return useSyncExternalStore(subscribe, getSnapshot)
32
78
  }
33
79
 
package/tsconfig.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "include": ["src/**/*"],
3
3
  "compilerOptions": {
4
+ "target": "ES2015",
4
5
  "strict": true,
5
6
  "outDir": "dist",
6
7
  "jsx": "react",