react-rx 2.1.2 → 3.0.0-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/cjs/WithObservable.d.ts +1 -1
  2. package/dist/cjs/__tests__/strictmode.test.js +43 -18
  3. package/dist/cjs/__tests__/useAsObservable.test.js +12 -11
  4. package/dist/cjs/__tests__/useObservable.test.js +31 -30
  5. package/dist/cjs/reactiveComponent.d.ts +2 -2
  6. package/dist/cjs/useAsObservable.d.ts +1 -0
  7. package/dist/cjs/useObservable.js +16 -48
  8. package/dist/es2015/WithObservable.d.ts +1 -1
  9. package/dist/es2015/__tests__/strictmode.test.js +34 -9
  10. package/dist/es2015/__tests__/useAsObservable.test.js +1 -0
  11. package/dist/es2015/__tests__/useObservable.test.js +1 -0
  12. package/dist/es2015/reactiveComponent.d.ts +2 -2
  13. package/dist/es2015/useAsObservable.d.ts +1 -0
  14. package/dist/es2015/useObservable.js +18 -50
  15. package/dist/esm/WithObservable.d.ts +1 -1
  16. package/dist/esm/__tests__/strictmode.test.js +35 -10
  17. package/dist/esm/__tests__/useAsObservable.test.js +1 -0
  18. package/dist/esm/__tests__/useObservable.test.js +1 -0
  19. package/dist/esm/reactiveComponent.d.ts +2 -2
  20. package/dist/esm/useAsObservable.d.ts +1 -0
  21. package/dist/esm/useObservable.js +18 -50
  22. package/package.json +21 -27
  23. package/src/__tests__/strictmode.test.ts +37 -9
  24. package/src/__tests__/useAsObservable.test.tsx +1 -0
  25. package/src/__tests__/useObservable.test.tsx +1 -0
  26. package/src/useAsObservable.ts +1 -0
  27. package/src/useObservable.ts +20 -70
  28. package/src/utils.ts +1 -1
  29. package/dist/cjs/useIsomorphicEffect.d.ts +0 -2
  30. package/dist/cjs/useIsomorphicEffect.js +0 -5
  31. package/dist/cjs/withPropsStream.d.ts +0 -8
  32. package/dist/cjs/withPropsStream.js +0 -55
  33. package/dist/es2015/useIsomorphicEffect.d.ts +0 -2
  34. package/dist/es2015/useIsomorphicEffect.js +0 -2
  35. package/dist/es2015/withPropsStream.d.ts +0 -8
  36. package/dist/es2015/withPropsStream.js +0 -17
  37. package/dist/esm/useIsomorphicEffect.d.ts +0 -2
  38. package/dist/esm/useIsomorphicEffect.js +0 -2
  39. package/dist/esm/withPropsStream.d.ts +0 -8
  40. package/dist/esm/withPropsStream.js +0 -28
  41. package/src/useIsomorphicEffect.ts +0 -3
  42. package/src/withPropsStream.tsx +0 -27
@@ -1,58 +1,26 @@
1
- import { useEffect, useMemo, useRef } from 'react';
2
- import { useSyncExternalStore } from 'use-sync-external-store/shim';
3
- import { shareReplay, tap } from 'rxjs/operators';
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { tap } from 'rxjs/operators';
4
3
  function getValue(value) {
5
4
  return typeof value === 'function' ? value() : value;
6
5
  }
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
- }
18
6
  export function useObservable(observable, initialValue) {
19
- const [getSnapshot, subscribe] = useMemo(() => {
20
- const store = getOrCreateStore(observable, getValue(initialValue));
21
- if (store.subscription.closed) {
22
- store.subscription = store.observable.subscribe();
23
- }
24
- return [
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);
32
- return () => {
33
- sub.unsubscribe();
34
- };
35
- },
36
- ];
37
- }, [observable]);
38
- const shouldRestoreSubscriptionRef = useRef(false);
7
+ const [value, setValue] = useState(() => getValue(initialValue));
8
+ const initialValueRef = useRef(initialValue);
9
+ initialValueRef.current = initialValue;
39
10
  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]);
54
- return useSyncExternalStore(subscribe, getSnapshot);
11
+ setValue(getValue(initialValueRef.current));
12
+ const subscription = observable.pipe(tap(next => setValue(next))).subscribe();
13
+ return () => subscription.unsubscribe();
14
+ }, [initialValueRef, observable]);
15
+ return value;
55
16
  }
56
17
  export function useMemoObservable(observableOrFactory, deps, initialValue) {
57
- return useObservable(useMemo(() => getValue(observableOrFactory), deps), initialValue);
18
+ const [value, setValue] = useState(() => getValue(initialValue));
19
+ useEffect(() => {
20
+ const subscription = getValue(observableOrFactory)
21
+ .pipe(tap(next => setValue(next)))
22
+ .subscribe();
23
+ return () => subscription.unsubscribe();
24
+ }, deps);
25
+ return value;
58
26
  }
@@ -4,7 +4,7 @@ interface Props<T> {
4
4
  observable: Observable<T>;
5
5
  children: (value: T) => ReactNode;
6
6
  }
7
- declare type ObservableComponent<T> = ComponentType<Props<T>>;
7
+ type ObservableComponent<T> = ComponentType<Props<T>>;
8
8
  /**
9
9
  * @deprecated Use the useObservable hook instead
10
10
  */
@@ -13,7 +13,7 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
13
13
  function verb(n) { return function (v) { return step([n, v]); }; }
14
14
  function step(op) {
15
15
  if (f) throw new TypeError("Generator is already executing.");
16
- while (_) try {
16
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
17
17
  if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
18
18
  if (y = 0, t) op = [op[0] & 2, t.value];
19
19
  switch (op[0]) {
@@ -35,18 +35,19 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
35
35
  }
36
36
  };
37
37
  import { BehaviorSubject, Observable } from 'rxjs';
38
- import { useObservable } from '../useObservable';
38
+ import { useMemoObservable, useObservable } from '../useObservable';
39
39
  import { createElement, Fragment, StrictMode, useEffect } from 'react';
40
40
  import { act, render } from '@testing-library/react';
41
41
  import { useAsObservable } from '../useAsObservable';
42
+ import { test, expect } from 'vitest';
42
43
  var wait = function (ms) { return new Promise(function (resolve) { return setTimeout(resolve, ms); }); };
43
44
  // NOTE: Jest runs NODE_ENV=test by default, which enables development flags for React
44
45
  test('Strict mode should trigger double mount effects and re-renders', function () { return __awaiter(void 0, void 0, void 0, function () {
45
46
  function ObservableComponent() {
47
+ var observedValue = useObservable(observable, 0);
46
48
  useEffect(function () {
47
49
  mountCount++;
48
50
  }, []);
49
- var observedValue = useObservable(observable);
50
51
  returnedValues.push(observedValue);
51
52
  return createElement(Fragment, null, observedValue);
52
53
  }
@@ -59,7 +60,7 @@ test('Strict mode should trigger double mount effects and re-renders', function
59
60
  returnedValues = [];
60
61
  mountCount = 0;
61
62
  rerender = render(createElement(StrictMode, null, createElement(ObservableComponent))).rerender;
62
- expect(mountCount).toEqual(2);
63
+ expect(mountCount).toBe(2);
63
64
  expect(returnedValues).toEqual([0, 0]);
64
65
  return [4 /*yield*/, wait(10)];
65
66
  case 1:
@@ -74,11 +75,14 @@ test('Strict mode should trigger double mount effects and re-renders', function
74
75
  });
75
76
  }); });
76
77
  test('Strict mode should unsubscribe the source observable on unmount', function () {
77
- var subscribed = false;
78
+ var subscribed = [];
79
+ var unsubscribed = [];
80
+ var nextId = 0;
78
81
  var observable = new Observable(function () {
79
- subscribed = true;
82
+ var id = nextId++;
83
+ subscribed.push(id);
80
84
  return function () {
81
- subscribed = false;
85
+ unsubscribed.push(id);
82
86
  };
83
87
  });
84
88
  function ObservableComponent() {
@@ -86,14 +90,35 @@ test('Strict mode should unsubscribe the source observable on unmount', function
86
90
  return createElement(Fragment, null);
87
91
  }
88
92
  var rerender = render(createElement(StrictMode, null, createElement(ObservableComponent))).rerender;
89
- expect(subscribed).toBe(true);
93
+ expect(subscribed).toEqual([0, 1]);
94
+ rerender(createElement(StrictMode, null, createElement('div')));
95
+ expect(unsubscribed).toEqual([0, 1]);
96
+ });
97
+ test('useMemoObservable should unsubscribe the source observable on unmount', function () {
98
+ var subscribed = [];
99
+ var unsubscribed = [];
100
+ var nextId = 0;
101
+ function ObservableComponent() {
102
+ useMemoObservable(function () {
103
+ return new Observable(function () {
104
+ var id = nextId++;
105
+ subscribed.push(id);
106
+ return function () {
107
+ unsubscribed.push(id);
108
+ };
109
+ });
110
+ }, []);
111
+ return createElement(Fragment, null);
112
+ }
113
+ var rerender = render(createElement(StrictMode, null, createElement(ObservableComponent))).rerender;
114
+ expect(subscribed).toEqual([0, 1]);
90
115
  rerender(createElement(StrictMode, null, createElement('div')));
91
- expect(subscribed).toBe(false);
116
+ expect(unsubscribed).toEqual([0, 1]);
92
117
  });
93
118
  test('useAsObservable should work in strict mode', function () { return __awaiter(void 0, void 0, void 0, function () {
94
119
  function ObservableComponent(props) {
95
120
  var count$ = useAsObservable(props.count);
96
- var count = useObservable(count$);
121
+ var count = useObservable(count$, props.count);
97
122
  returnedValues.push(count);
98
123
  return createElement(Fragment, null, 'ok');
99
124
  }
@@ -1,6 +1,7 @@
1
1
  import { useAsObservable } from '../useAsObservable';
2
2
  import React from 'react';
3
3
  import { renderHook } from '@testing-library/react';
4
+ import { test, expect } from 'vitest';
4
5
  test('the returned observable should receive a new value when component is rendered with a new value', function () {
5
6
  var receivedValues = [];
6
7
  var _a = renderHook(function (props) {
@@ -3,6 +3,7 @@ import { useObservable } from '../useObservable';
3
3
  import { asyncScheduler, Observable, of, scheduled, Subject, timer } from 'rxjs';
4
4
  import { mapTo } from 'rxjs/operators';
5
5
  import { createElement, Fragment } from 'react';
6
+ import { test, expect } from 'vitest';
6
7
  test('should subscribe immediately on component mount and unsubscribe on component unmount', function () {
7
8
  var subscribed = false;
8
9
  var observable = new Observable(function () {
@@ -1,8 +1,8 @@
1
1
  import { Observable } from 'rxjs';
2
2
  import { FunctionComponent, ReactNode, Ref } from 'react';
3
- declare type Component<Props> = (input$: Observable<Props>) => Observable<ReactNode>;
3
+ type Component<Props> = (input$: Observable<Props>) => Observable<ReactNode>;
4
4
  export declare function reactiveComponent<Props>(observable: Observable<Props>): FunctionComponent<Props>;
5
5
  export declare function reactiveComponent<Props>(component: Component<Props>): FunctionComponent<Props>;
6
- declare type ForwardRefComponent<RefType, Props> = (input$: Observable<Props>, ref: Ref<RefType>) => Observable<ReactNode>;
6
+ 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>>;
8
8
  export {};
@@ -4,6 +4,7 @@ import { Observable } from 'rxjs';
4
4
  * Returns an observable representing updates to any React value (props, state or any other calculated value)
5
5
  * Note: the returned observable is the same instance throughout the component lifecycle
6
6
  * @param value
7
+ * @deprecated use an `of` operator and `useMemoObservable` instead for a faster, more robust and siimpler solution
7
8
  */
8
9
  export declare function useAsObservable<T>(value: T): Observable<T>;
9
10
  export declare function useAsObservable<T, K>(value: T, operator: (input: Observable<T>) => Observable<K>): Observable<K>;
@@ -1,58 +1,26 @@
1
- import { useEffect, useMemo, useRef } from 'react';
2
- import { useSyncExternalStore } from 'use-sync-external-store/shim';
3
- import { shareReplay, tap } from 'rxjs/operators';
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { tap } from 'rxjs/operators';
4
3
  function getValue(value) {
5
4
  return typeof value === 'function' ? value() : value;
6
5
  }
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
- }
18
6
  export function useObservable(observable, initialValue) {
19
- var _a = useMemo(function () {
20
- var store = getOrCreateStore(observable, getValue(initialValue));
21
- if (store.subscription.closed) {
22
- store.subscription = store.observable.subscribe();
23
- }
24
- return [
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);
32
- return function () {
33
- sub.unsubscribe();
34
- };
35
- },
36
- ];
37
- }, [observable]), getSnapshot = _a[0], subscribe = _a[1];
38
- var shouldRestoreSubscriptionRef = useRef(false);
7
+ var _a = useState(function () { return getValue(initialValue); }), value = _a[0], setValue = _a[1];
8
+ var initialValueRef = useRef(initialValue);
9
+ initialValueRef.current = initialValue;
39
10
  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]);
54
- return useSyncExternalStore(subscribe, getSnapshot);
11
+ setValue(getValue(initialValueRef.current));
12
+ var subscription = observable.pipe(tap(function (next) { return setValue(next); })).subscribe();
13
+ return function () { return subscription.unsubscribe(); };
14
+ }, [initialValueRef, observable]);
15
+ return value;
55
16
  }
56
17
  export function useMemoObservable(observableOrFactory, deps, initialValue) {
57
- return useObservable(useMemo(function () { return getValue(observableOrFactory); }, deps), initialValue);
18
+ var _a = useState(function () { return getValue(initialValue); }), value = _a[0], setValue = _a[1];
19
+ useEffect(function () {
20
+ var subscription = getValue(observableOrFactory)
21
+ .pipe(tap(function (next) { return setValue(next); }))
22
+ .subscribe();
23
+ return function () { return subscription.unsubscribe(); };
24
+ }, deps);
25
+ return value;
58
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-rx",
3
- "version": "2.1.2",
3
+ "version": "3.0.0-0",
4
4
  "description": "React + RxJS = <3",
5
5
  "keywords": [
6
6
  "action",
@@ -28,7 +28,6 @@
28
28
  "sync",
29
29
  "typesafe",
30
30
  "typescript",
31
- "use-sync-external-store",
32
31
  "use"
33
32
  ],
34
33
  "homepage": "https://react-rx.dev",
@@ -65,42 +64,37 @@
65
64
  "dev": "cd website && npm run dev",
66
65
  "prepublishOnly": "npm run clean && npm run build",
67
66
  "watch": "run-p \"build:* -- --watch\"",
68
- "test": "jest",
67
+ "test": "vitest run",
69
68
  "lint": "eslint --cache ."
70
69
  },
71
70
  "dependencies": {
72
- "observable-callback": "^1.0.2",
73
- "use-sync-external-store": "^1.2.0"
71
+ "observable-callback": "^1.0.2"
74
72
  },
75
73
  "devDependencies": {
76
- "@sanity/semantic-release-preset": "^2.0.2",
77
- "@testing-library/dom": "^8.14.0",
78
- "@testing-library/react": "^13.3.0",
79
- "@types/jest": "^28.1.4",
80
- "@types/node": "^18.8.2",
81
- "@types/react": "^18.0.12",
82
- "@types/react-dom": "^18.0.5",
83
- "@types/use-sync-external-store": "^0.0.3",
84
- "@typescript-eslint/eslint-plugin": "^5.39.0",
85
- "@typescript-eslint/parser": "^5.39.0",
86
- "eslint": "^8.24.0",
87
- "eslint-config-prettier": "^8.1.0",
74
+ "@sanity/semantic-release-preset": "^4.0.0",
75
+ "@testing-library/dom": "^9.0.1",
76
+ "@testing-library/react": "^14.0.0",
77
+ "@types/node": "^18.14.6",
78
+ "@types/react": "^18.0.28",
79
+ "@types/react-dom": "^18.0.11",
80
+ "@typescript-eslint/eslint-plugin": "^5.54.1",
81
+ "@typescript-eslint/parser": "^5.54.1",
82
+ "eslint": "^8.35.0",
83
+ "eslint-config-prettier": "^8.7.0",
88
84
  "eslint-plugin-prettier": "^4.2.1",
89
- "eslint-plugin-react": "^7.22.0",
85
+ "eslint-plugin-react": "^7.32.2",
90
86
  "eslint-plugin-react-hooks": "^4.6.0",
91
- "jest": "^28.1.2",
92
- "jest-environment-jsdom": "^28.1.2",
93
- "jsdom": "^20.0.0",
87
+ "jsdom": "^21.1.0",
94
88
  "npm-run-all": "^4.1.5",
95
- "prettier": "^2.7.1",
96
- "prettier-plugin-packagejson": "^2.3.0",
89
+ "prettier": "^2.8.4",
90
+ "prettier-plugin-packagejson": "^2.4.3",
97
91
  "react": "^18.2.0",
98
92
  "react-dom": "^18.2.0",
99
93
  "react-test-renderer": "^18.2.0",
100
- "rimraf": "^3.0.2",
101
- "rxjs": "^6.5.5",
102
- "ts-jest": "^28.0.5",
103
- "typescript": "4.7.4"
94
+ "rimraf": "^4.3.1",
95
+ "rxjs": "^7.8.0",
96
+ "typescript": "4.9.5",
97
+ "vitest": "^0.29.2"
104
98
  },
105
99
  "peerDependencies": {
106
100
  "react": "^16.8 || ^17 || ^18",
@@ -1,8 +1,9 @@
1
1
  import {BehaviorSubject, Observable, of} from 'rxjs'
2
- import {useObservable} from '../useObservable'
2
+ import {useMemoObservable, useObservable} from '../useObservable'
3
3
  import {createElement, Fragment, StrictMode, useEffect} from 'react'
4
4
  import {act, render} from '@testing-library/react'
5
5
  import {useAsObservable} from '../useAsObservable'
6
+ import {test, expect} from 'vitest'
6
7
 
7
8
  const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
8
9
 
@@ -15,16 +16,17 @@ test('Strict mode should trigger double mount effects and re-renders', async ()
15
16
  const returnedValues: unknown[] = []
16
17
  let mountCount = 0
17
18
  function ObservableComponent() {
19
+ const observedValue = useObservable(observable, 0)
18
20
  useEffect(() => {
19
21
  mountCount++
20
22
  }, [])
21
- const observedValue = useObservable(observable)
22
23
  returnedValues.push(observedValue)
23
24
  return createElement(Fragment, null, observedValue)
24
25
  }
25
26
 
26
27
  const {rerender} = render(createElement(StrictMode, null, createElement(ObservableComponent)))
27
- expect(mountCount).toEqual(2)
28
+
29
+ expect(mountCount).toBe(2)
28
30
 
29
31
  expect(returnedValues).toEqual([0, 0])
30
32
 
@@ -39,11 +41,14 @@ test('Strict mode should trigger double mount effects and re-renders', async ()
39
41
  })
40
42
 
41
43
  test('Strict mode should unsubscribe the source observable on unmount', () => {
42
- let subscribed = false
44
+ const subscribed: number[] = []
45
+ const unsubscribed: number[] = []
46
+ let nextId = 0
43
47
  const observable = new Observable(() => {
44
- subscribed = true
48
+ const id = nextId++
49
+ subscribed.push(id)
45
50
  return () => {
46
- subscribed = false
51
+ unsubscribed.push(id)
47
52
  }
48
53
  })
49
54
 
@@ -53,16 +58,39 @@ test('Strict mode should unsubscribe the source observable on unmount', () => {
53
58
  }
54
59
 
55
60
  const {rerender} = render(createElement(StrictMode, null, createElement(ObservableComponent)))
56
- expect(subscribed).toBe(true)
61
+ expect(subscribed).toEqual([0, 1])
62
+ rerender(createElement(StrictMode, null, createElement('div')))
63
+ expect(unsubscribed).toEqual([0, 1])
64
+ })
65
+
66
+ test('useMemoObservable should unsubscribe the source observable on unmount', () => {
67
+ const subscribed: number[] = []
68
+ const unsubscribed: number[] = []
69
+ let nextId = 0
70
+ function ObservableComponent() {
71
+ useMemoObservable(() => {
72
+ return new Observable(() => {
73
+ const id = nextId++
74
+ subscribed.push(id)
75
+ return () => {
76
+ unsubscribed.push(id)
77
+ }
78
+ })
79
+ }, [])
80
+ return createElement(Fragment, null)
81
+ }
82
+
83
+ const {rerender} = render(createElement(StrictMode, null, createElement(ObservableComponent)))
84
+ expect(subscribed).toEqual([0, 1])
57
85
  rerender(createElement(StrictMode, null, createElement('div')))
58
- expect(subscribed).toBe(false)
86
+ expect(unsubscribed).toEqual([0, 1])
59
87
  })
60
88
 
61
89
  test('useAsObservable should work in strict mode', async () => {
62
90
  const returnedValues: unknown[] = []
63
91
  function ObservableComponent(props: {count: number}) {
64
92
  const count$ = useAsObservable(props.count)
65
- const count = useObservable(count$)
93
+ const count = useObservable(count$, props.count)
66
94
  returnedValues.push(count)
67
95
  return createElement(Fragment, null, 'ok')
68
96
  }
@@ -2,6 +2,7 @@ import {useAsObservable} from '../useAsObservable'
2
2
  import React from 'react'
3
3
  import {renderHook} from '@testing-library/react'
4
4
  import {Observable} from 'rxjs'
5
+ import {test, expect} from 'vitest'
5
6
 
6
7
  test('the returned observable should receive a new value when component is rendered with a new value', () => {
7
8
  const receivedValues: string[] = []
@@ -3,6 +3,7 @@ import {useObservable} from '../useObservable'
3
3
  import {asyncScheduler, Observable, of, scheduled, Subject, timer} from 'rxjs'
4
4
  import {mapTo} from 'rxjs/operators'
5
5
  import {createElement, Fragment} from 'react'
6
+ import {test, expect} from 'vitest'
6
7
 
7
8
  test('should subscribe immediately on component mount and unsubscribe on component unmount', () => {
8
9
  let subscribed = false
@@ -7,6 +7,7 @@ import {distinctUntilChanged} from 'rxjs/operators'
7
7
  * Returns an observable representing updates to any React value (props, state or any other calculated value)
8
8
  * Note: the returned observable is the same instance throughout the component lifecycle
9
9
  * @param value
10
+ * @deprecated use an `of` operator and `useMemoObservable` instead for a faster, more robust and siimpler solution
10
11
  */
11
12
  export function useAsObservable<T>(value: T): Observable<T>
12
13
  export function useAsObservable<T, K>(
@@ -1,80 +1,25 @@
1
- import {Observable, Subscription} from 'rxjs'
2
- import {DependencyList, useEffect, useMemo, useRef} from 'react'
3
- import {useSyncExternalStore} from 'use-sync-external-store/shim'
4
- import {shareReplay, tap} from 'rxjs/operators'
1
+ import {Observable} from 'rxjs'
2
+ import {DependencyList, useEffect, useRef, useState} from 'react'
3
+ import {tap} from 'rxjs/operators'
5
4
 
6
5
  function getValue<T>(value: T): T extends () => infer U ? U : T {
7
6
  return typeof value === 'function' ? value() : value
8
7
  }
9
8
 
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
-
33
9
  export function useObservable<T>(observable: Observable<T>): T | undefined
34
10
  export function useObservable<T>(observable: Observable<T>, initialValue: T): T
35
11
  export function useObservable<T>(observable: Observable<T>, initialValue: () => T): T
36
12
  export function useObservable<T>(observable: Observable<T>, initialValue?: T | (() => T)) {
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
- }
44
- return [
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)
52
- return () => {
53
- sub.unsubscribe()
54
- }
55
- },
56
- ]
57
- }, [observable])
58
-
59
- const shouldRestoreSubscriptionRef = useRef(false)
13
+ const [value, setValue] = useState(() => getValue(initialValue))
14
+ const initialValueRef = useRef(initialValue)
15
+ initialValueRef.current = initialValue
60
16
  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])
17
+ setValue(getValue(initialValueRef.current))
18
+ const subscription = observable.pipe(tap(next => setValue(next))).subscribe()
19
+ return () => subscription.unsubscribe()
20
+ }, [initialValueRef, observable])
76
21
 
77
- return useSyncExternalStore(subscribe, getSnapshot)
22
+ return value
78
23
  }
79
24
 
80
25
  export function useMemoObservable<T>(
@@ -91,8 +36,13 @@ export function useMemoObservable<T>(
91
36
  deps: DependencyList,
92
37
  initialValue?: T | (() => T),
93
38
  ) {
94
- return useObservable(
95
- useMemo(() => getValue(observableOrFactory), deps),
96
- initialValue,
97
- )
39
+ const [value, setValue] = useState(() => getValue(initialValue))
40
+ useEffect(() => {
41
+ const subscription = getValue(observableOrFactory)
42
+ .pipe(tap(next => setValue(next)))
43
+ .subscribe()
44
+ return () => subscription.unsubscribe()
45
+ }, deps)
46
+
47
+ return value
98
48
  }
package/src/utils.ts CHANGED
@@ -4,7 +4,7 @@ import {observableCallback} from 'observable-callback'
4
4
  import {startWith} from 'rxjs/operators'
5
5
  import {Dispatch, SetStateAction, useContext, useRef, useState, Context} from 'react'
6
6
 
7
- const createState = <T>(initialState: T) => observableCallback(startWith<T, T>(initialState))
7
+ const createState = <T>(initialState: T) => observableCallback(startWith<T>(initialState))
8
8
 
9
9
  export function observeState<T>(
10
10
  initial: T | (() => T),
@@ -1,2 +0,0 @@
1
- import { useEffect } from 'react';
2
- export declare const useIsomorphicEffect: typeof useEffect;
@@ -1,5 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.useIsomorphicEffect = void 0;
4
- var react_1 = require("react");
5
- exports.useIsomorphicEffect = typeof window !== 'undefined' ? react_1.useLayoutEffect : react_1.useEffect;
@@ -1,8 +0,0 @@
1
- import * as React from 'react';
2
- import { Observable } from 'rxjs';
3
- declare type ObservableFactory<SourceProps, TargetProps> = (props$: Observable<SourceProps>) => Observable<TargetProps>;
4
- /**
5
- * @deprecated Use reactiveComponent instead
6
- */
7
- export declare function withPropsStream<SourceProps, TargetProps>(observableOrFactory: Observable<TargetProps> | ObservableFactory<SourceProps, TargetProps>, TargetComponent: React.ComponentType<TargetProps>): React.FunctionComponent<SourceProps>;
8
- export {};