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 +0 -3
- package/dist/cjs/reactiveComponent.d.ts +1 -1
- package/dist/cjs/useAsObservable.js +30 -6
- package/dist/cjs/useIsomorphicEffect.d.ts +2 -2
- package/dist/cjs/useObservable.js +40 -12
- package/dist/es2015/reactiveComponent.d.ts +1 -1
- package/dist/es2015/useAsObservable.js +30 -6
- package/dist/es2015/useIsomorphicEffect.d.ts +2 -2
- package/dist/es2015/useObservable.js +41 -13
- package/dist/esm/reactiveComponent.d.ts +1 -1
- package/dist/esm/useAsObservable.js +31 -7
- package/dist/esm/useIsomorphicEffect.d.ts +2 -2
- package/dist/esm/useObservable.js +41 -13
- package/jest.config.ts +14 -0
- package/package.json +13 -13
- package/src/reactiveComponent.ts +3 -3
- package/src/useAsObservable.ts +34 -7
- package/src/useObservable.ts +60 -14
- package/tsconfig.json +1 -0
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
|
|
6
|
+
var operators_1 = require("rxjs/operators");
|
|
7
7
|
function useAsObservable(value, operator) {
|
|
8
|
-
var
|
|
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
|
-
}, [])
|
|
13
|
-
(0,
|
|
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 {
|
|
2
|
-
export declare const useIsomorphicEffect: typeof
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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 () {
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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,
|
|
3
|
-
import {
|
|
2
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
3
|
+
import { distinctUntilChanged } from 'rxjs/operators';
|
|
4
4
|
export function useAsObservable(value, operator) {
|
|
5
|
-
const
|
|
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
|
-
|
|
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 {
|
|
2
|
-
export declare const useIsomorphicEffect: typeof
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
export declare const useIsomorphicEffect: typeof useEffect;
|
|
@@ -1,28 +1,56 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
()
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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,
|
|
3
|
-
import {
|
|
2
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
3
|
+
import { distinctUntilChanged } from 'rxjs/operators';
|
|
4
4
|
export function useAsObservable(value, operator) {
|
|
5
|
-
var
|
|
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
|
-
}, [])
|
|
10
|
-
|
|
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 {
|
|
2
|
-
export declare const useIsomorphicEffect: typeof
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
export declare const useIsomorphicEffect: typeof useEffect;
|
|
@@ -1,28 +1,56 @@
|
|
|
1
|
-
import {
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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 () {
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-rx",
|
|
3
|
-
"version": "2.0
|
|
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": "^
|
|
34
|
-
"@testing-library/react": "^
|
|
35
|
-
"@
|
|
36
|
-
"@types/
|
|
37
|
-
"@types/react": "^
|
|
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": "^
|
|
47
|
-
"jsdom": "^
|
|
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": "^
|
|
51
|
-
"react-dom": "^
|
|
52
|
-
"react-test-renderer": "^
|
|
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": "^
|
|
55
|
+
"ts-jest": "^28.0.5",
|
|
56
56
|
"typescript": "^4.2.2"
|
|
57
57
|
},
|
|
58
58
|
"repository": {
|
package/src/reactiveComponent.ts
CHANGED
|
@@ -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<
|
|
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<
|
|
38
|
+
observableOrComponent: Observable<ReactNode> | Component<Props>,
|
|
39
39
|
) {
|
|
40
40
|
return typeof observableOrComponent === 'function'
|
|
41
41
|
? fromComponent(observableOrComponent)
|
package/src/useAsObservable.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {BehaviorSubject, Observable} from 'rxjs'
|
|
2
|
-
import {useEffect,
|
|
3
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/src/useObservable.ts
CHANGED
|
@@ -1,33 +1,79 @@
|
|
|
1
|
-
import {Observable,
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
()
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
|