react-rx 3.0.0-2 → 3.0.0-4

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.
@@ -14,7 +14,7 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
14
14
  function verb(n) { return function (v) { return step([n, v]); }; }
15
15
  function step(op) {
16
16
  if (f) throw new TypeError("Generator is already executing.");
17
- while (_) try {
17
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
18
  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;
19
19
  if (y = 0, t) op = [op[0] & 2, t.value];
20
20
  switch (op[0]) {
@@ -1,4 +1,4 @@
1
- import { Observable } from 'rxjs';
1
+ import type { Observable } from 'rxjs';
2
2
  export declare function useObservable<ObservableType extends Observable<any>>(observable: ObservableType, initialValue?: UnboxObservable<ObservableType> | (() => UnboxObservable<ObservableType>)): UnboxObservable<ObservableType>;
3
- declare type UnboxObservable<T> = T extends Observable<infer U> ? U : never;
3
+ type UnboxObservable<T> = T extends Observable<infer U> ? U : never;
4
4
  export {};
@@ -7,16 +7,6 @@ function getValue(value) {
7
7
  return typeof value === 'function' ? value() : value;
8
8
  }
9
9
  var cache = new WeakMap();
10
- function getOrCreateStore(inputObservable, initialValue) {
11
- if (!cache.has(inputObservable)) {
12
- var entry_1 = { currentValue: initialValue };
13
- entry_1.observable = inputObservable.pipe((0, operators_1.shareReplay)({ refCount: true, bufferSize: 1 }), (0, operators_1.tap)(function (value) { return (entry_1.currentValue = value); }));
14
- // Eagerly subscribe to sync set `entry.currentValue` to what the observable returns
15
- entry_1.subscription = entry_1.observable.subscribe();
16
- cache.set(inputObservable, entry_1);
17
- }
18
- return cache.get(inputObservable);
19
- }
20
10
  function useObservable(observable, initialValue) {
21
11
  /**
22
12
  * Store the initialValue in a ref, as we don't want a changed `initialValue` to trigger a re-subscription.
@@ -29,41 +19,30 @@ function useObservable(observable, initialValue) {
29
19
  (0, react_1.useEffect)(function () {
30
20
  initialValueRef.current = getValue(initialValue);
31
21
  }, [initialValue]);
32
- var _a = (0, react_1.useMemo)(function () {
33
- var store = getOrCreateStore(observable, initialValueRef.current);
34
- if (store.subscription.closed) {
35
- store.subscription = store.observable.subscribe();
22
+ var store = (0, react_1.useMemo)(function () {
23
+ if (!cache.has(observable)) {
24
+ var entry_1 = {
25
+ snapshot: initialValueRef.current,
26
+ };
27
+ entry_1.observable = observable.pipe((0, operators_1.shareReplay)({ refCount: true, bufferSize: 1 }), (0, operators_1.tap)(function (value) { return (entry_1.snapshot = value); }));
28
+ // Eagerly subscribe to sync set `entry.currentValue` to what the observable returns, and keep the observable alive until the component unmounts.
29
+ entry_1.subscription = entry_1.observable.subscribe();
30
+ cache.set(observable, entry_1);
36
31
  }
37
- return [
38
- function getSnapshot() {
39
- // @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.
40
- return store.currentValue;
41
- },
42
- function subscribe(callback) {
43
- // @TODO: perf opt opportunity: we could do `store.subscription.unsubscribe()` here as we only need 1 subscription active to keep the observer alive
44
- var sub = store.observable.subscribe(callback);
45
- return function () {
46
- sub.unsubscribe();
47
- };
48
- },
49
- ];
50
- }, [observable]), getSnapshot = _a[0], subscribe = _a[1];
51
- var shouldRestoreSubscriptionRef = (0, react_1.useRef)(false);
52
- (0, react_1.useEffect)(function () {
53
- var store = getOrCreateStore(observable, initialValueRef.current);
54
- if (shouldRestoreSubscriptionRef.current) {
55
- if (store.subscription.closed) {
56
- store.subscription = store.observable.subscribe();
57
- }
58
- shouldRestoreSubscriptionRef.current = false;
32
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
33
+ var instance = cache.get(observable);
34
+ if (instance.subscription.closed) {
35
+ instance.subscription = instance.observable.subscribe();
59
36
  }
60
- return function () {
61
- // React StrictMode will call effects as `setup + teardown + setup` thus we can't trust this callback as "react is about to unmount"
62
- // 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
63
- shouldRestoreSubscriptionRef.current = !store.subscription.closed;
64
- store.subscription.unsubscribe();
37
+ return {
38
+ subscribe: function (onStoreChange) {
39
+ var subscription = instance.observable.subscribe(onStoreChange);
40
+ instance.subscription.unsubscribe();
41
+ return function () { return subscription.unsubscribe(); };
42
+ },
43
+ getSnapshot: function () { return instance.snapshot; },
65
44
  };
66
45
  }, [observable]);
67
- return (0, react_1.useSyncExternalStore)(subscribe, getSnapshot);
46
+ return (0, react_1.useSyncExternalStore)(store.subscribe, store.getSnapshot);
68
47
  }
69
48
  exports.useObservable = useObservable;
@@ -1,3 +1,8 @@
1
1
  import { DependencyList } from 'react';
2
2
  import { Observable } from 'rxjs';
3
3
  export declare function useObservableCallback<T, U>(fn: (arg: Observable<T>) => Observable<U>, dependencies?: DependencyList): (arg: T) => void;
4
+ export declare function useObservableEvent<T, U>(fn: (arg: Observable<T>) => Observable<U>): (arg: T) => void;
5
+ /**
6
+ * This is a ponyfill of the upcoming `useEffectEvent` hook that'll arrive in React 19
7
+ */
8
+ export declare function useEffectEvent<const T extends (...args: any[]) => void>(fn: T): T;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.useObservableCallback = void 0;
3
+ exports.useEffectEvent = exports.useObservableEvent = exports.useObservableCallback = void 0;
4
4
  var observable_callback_1 = require("observable-callback");
5
5
  var react_1 = require("react");
6
6
  var EMPTY_DEPS = [];
@@ -21,3 +21,32 @@ function useObservableCallback(fn, dependencies) {
21
21
  return call;
22
22
  }
23
23
  exports.useObservableCallback = useObservableCallback;
24
+ function useObservableEvent(fn) {
25
+ var _a = (0, react_1.useState)(function () { return (0, observable_callback_1.observableCallback)(); })[0], calls$ = _a[0], call = _a[1];
26
+ var callback = useEffectEvent(fn);
27
+ (0, react_1.useEffect)(function () {
28
+ var subscription = calls$.pipe(callback).subscribe();
29
+ return function () { return subscription.unsubscribe(); };
30
+ }, [callback, calls$]);
31
+ return call;
32
+ }
33
+ exports.useObservableEvent = useObservableEvent;
34
+ /**
35
+ * This is a ponyfill of the upcoming `useEffectEvent` hook that'll arrive in React 19
36
+ */
37
+ function useEffectEvent(fn) {
38
+ var ref = (0, react_1.useRef)(null);
39
+ (0, react_1.useInsertionEffect)(function () {
40
+ ref.current = fn;
41
+ }, [fn]);
42
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
43
+ return (0, react_1.useCallback)(function () {
44
+ var args = [];
45
+ for (var _i = 0; _i < arguments.length; _i++) {
46
+ args[_i] = arguments[_i];
47
+ }
48
+ var f = ref.current;
49
+ return f.apply(void 0, args);
50
+ }, []);
51
+ }
52
+ exports.useEffectEvent = useEffectEvent;
@@ -11,7 +11,7 @@ import { act, render } from '@testing-library/react';
11
11
  import { createElement, Fragment, StrictMode, useEffect } from 'react';
12
12
  import { BehaviorSubject, Observable } from 'rxjs';
13
13
  import { useObservable } from '../useObservable';
14
- const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));
14
+ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
15
15
  // NOTE: Jest runs NODE_ENV=test by default, which enables development flags for React
16
16
  test('Strict mode should trigger double mount effects and re-renders', () => __awaiter(void 0, void 0, void 0, function* () {
17
17
  const subject = new BehaviorSubject(0);
@@ -1,4 +1,4 @@
1
- import { Observable } from 'rxjs';
1
+ import type { Observable } from 'rxjs';
2
2
  export declare function useObservable<ObservableType extends Observable<any>>(observable: ObservableType, initialValue?: UnboxObservable<ObservableType> | (() => UnboxObservable<ObservableType>)): UnboxObservable<ObservableType>;
3
- declare type UnboxObservable<T> = T extends Observable<infer U> ? U : never;
3
+ type UnboxObservable<T> = T extends Observable<infer U> ? U : never;
4
4
  export {};
@@ -4,16 +4,6 @@ function getValue(value) {
4
4
  return typeof value === 'function' ? value() : value;
5
5
  }
6
6
  const cache = new WeakMap();
7
- function getOrCreateStore(inputObservable, initialValue) {
8
- if (!cache.has(inputObservable)) {
9
- const entry = { currentValue: initialValue };
10
- entry.observable = inputObservable.pipe(shareReplay({ refCount: true, bufferSize: 1 }), tap(value => (entry.currentValue = value)));
11
- // Eagerly subscribe to sync set `entry.currentValue` to what the observable returns
12
- entry.subscription = entry.observable.subscribe();
13
- cache.set(inputObservable, entry);
14
- }
15
- return cache.get(inputObservable);
16
- }
17
7
  export function useObservable(observable, initialValue) {
18
8
  /**
19
9
  * Store the initialValue in a ref, as we don't want a changed `initialValue` to trigger a re-subscription.
@@ -26,40 +16,29 @@ export function useObservable(observable, initialValue) {
26
16
  useEffect(() => {
27
17
  initialValueRef.current = getValue(initialValue);
28
18
  }, [initialValue]);
29
- const [getSnapshot, subscribe] = useMemo(() => {
30
- const store = getOrCreateStore(observable, initialValueRef.current);
31
- if (store.subscription.closed) {
32
- store.subscription = store.observable.subscribe();
19
+ const store = useMemo(() => {
20
+ if (!cache.has(observable)) {
21
+ const entry = {
22
+ snapshot: initialValueRef.current,
23
+ };
24
+ entry.observable = observable.pipe(shareReplay({ refCount: true, bufferSize: 1 }), tap((value) => (entry.snapshot = value)));
25
+ // Eagerly subscribe to sync set `entry.currentValue` to what the observable returns, and keep the observable alive until the component unmounts.
26
+ entry.subscription = entry.observable.subscribe();
27
+ cache.set(observable, entry);
33
28
  }
34
- return [
35
- function getSnapshot() {
36
- // @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.
37
- return store.currentValue;
38
- },
39
- function subscribe(callback) {
40
- // @TODO: perf opt opportunity: we could do `store.subscription.unsubscribe()` here as we only need 1 subscription active to keep the observer alive
41
- const sub = store.observable.subscribe(callback);
42
- return () => {
43
- sub.unsubscribe();
44
- };
45
- },
46
- ];
47
- }, [observable]);
48
- const shouldRestoreSubscriptionRef = useRef(false);
49
- useEffect(() => {
50
- const store = getOrCreateStore(observable, initialValueRef.current);
51
- if (shouldRestoreSubscriptionRef.current) {
52
- if (store.subscription.closed) {
53
- store.subscription = store.observable.subscribe();
54
- }
55
- shouldRestoreSubscriptionRef.current = false;
29
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
30
+ const instance = cache.get(observable);
31
+ if (instance.subscription.closed) {
32
+ instance.subscription = instance.observable.subscribe();
56
33
  }
57
- return () => {
58
- // React StrictMode will call effects as `setup + teardown + setup` thus we can't trust this callback as "react is about to unmount"
59
- // 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
60
- shouldRestoreSubscriptionRef.current = !store.subscription.closed;
61
- store.subscription.unsubscribe();
34
+ return {
35
+ subscribe: (onStoreChange) => {
36
+ const subscription = instance.observable.subscribe(onStoreChange);
37
+ instance.subscription.unsubscribe();
38
+ return () => subscription.unsubscribe();
39
+ },
40
+ getSnapshot: () => instance.snapshot,
62
41
  };
63
42
  }, [observable]);
64
- return useSyncExternalStore(subscribe, getSnapshot);
43
+ return useSyncExternalStore(store.subscribe, store.getSnapshot);
65
44
  }
@@ -1,3 +1,8 @@
1
1
  import { DependencyList } from 'react';
2
2
  import { Observable } from 'rxjs';
3
3
  export declare function useObservableCallback<T, U>(fn: (arg: Observable<T>) => Observable<U>, dependencies?: DependencyList): (arg: T) => void;
4
+ export declare function useObservableEvent<T, U>(fn: (arg: Observable<T>) => Observable<U>): (arg: T) => void;
5
+ /**
6
+ * This is a ponyfill of the upcoming `useEffectEvent` hook that'll arrive in React 19
7
+ */
8
+ export declare function useEffectEvent<const T extends (...args: any[]) => void>(fn: T): T;
@@ -1,5 +1,5 @@
1
1
  import { observableCallback } from 'observable-callback';
2
- import { useCallback, useEffect, useRef } from 'react';
2
+ import { useCallback, useEffect, useInsertionEffect, useRef, useState } from 'react';
3
3
  const EMPTY_DEPS = [];
4
4
  export function useObservableCallback(fn, dependencies = EMPTY_DEPS) {
5
5
  const callbackRef = useRef();
@@ -16,3 +16,26 @@ export function useObservableCallback(fn, dependencies = EMPTY_DEPS) {
16
16
  }, [calls$, call, callback]);
17
17
  return call;
18
18
  }
19
+ export function useObservableEvent(fn) {
20
+ const [[calls$, call]] = useState(() => observableCallback());
21
+ const callback = useEffectEvent(fn);
22
+ useEffect(() => {
23
+ const subscription = calls$.pipe(callback).subscribe();
24
+ return () => subscription.unsubscribe();
25
+ }, [callback, calls$]);
26
+ return call;
27
+ }
28
+ /**
29
+ * This is a ponyfill of the upcoming `useEffectEvent` hook that'll arrive in React 19
30
+ */
31
+ export function useEffectEvent(fn) {
32
+ const ref = useRef(null);
33
+ useInsertionEffect(() => {
34
+ ref.current = fn;
35
+ }, [fn]);
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ return useCallback((...args) => {
38
+ const f = ref.current;
39
+ return f(...args);
40
+ }, []);
41
+ }
@@ -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]) {
@@ -1,4 +1,4 @@
1
- import { Observable } from 'rxjs';
1
+ import type { Observable } from 'rxjs';
2
2
  export declare function useObservable<ObservableType extends Observable<any>>(observable: ObservableType, initialValue?: UnboxObservable<ObservableType> | (() => UnboxObservable<ObservableType>)): UnboxObservable<ObservableType>;
3
- declare type UnboxObservable<T> = T extends Observable<infer U> ? U : never;
3
+ type UnboxObservable<T> = T extends Observable<infer U> ? U : never;
4
4
  export {};
@@ -4,16 +4,6 @@ function getValue(value) {
4
4
  return typeof value === 'function' ? value() : value;
5
5
  }
6
6
  var cache = new WeakMap();
7
- function getOrCreateStore(inputObservable, initialValue) {
8
- if (!cache.has(inputObservable)) {
9
- var entry_1 = { currentValue: initialValue };
10
- entry_1.observable = inputObservable.pipe(shareReplay({ refCount: true, bufferSize: 1 }), tap(function (value) { return (entry_1.currentValue = value); }));
11
- // Eagerly subscribe to sync set `entry.currentValue` to what the observable returns
12
- entry_1.subscription = entry_1.observable.subscribe();
13
- cache.set(inputObservable, entry_1);
14
- }
15
- return cache.get(inputObservable);
16
- }
17
7
  export function useObservable(observable, initialValue) {
18
8
  /**
19
9
  * Store the initialValue in a ref, as we don't want a changed `initialValue` to trigger a re-subscription.
@@ -26,40 +16,29 @@ export function useObservable(observable, initialValue) {
26
16
  useEffect(function () {
27
17
  initialValueRef.current = getValue(initialValue);
28
18
  }, [initialValue]);
29
- var _a = useMemo(function () {
30
- var store = getOrCreateStore(observable, initialValueRef.current);
31
- if (store.subscription.closed) {
32
- store.subscription = store.observable.subscribe();
19
+ var store = useMemo(function () {
20
+ if (!cache.has(observable)) {
21
+ var entry_1 = {
22
+ snapshot: initialValueRef.current,
23
+ };
24
+ entry_1.observable = observable.pipe(shareReplay({ refCount: true, bufferSize: 1 }), tap(function (value) { return (entry_1.snapshot = value); }));
25
+ // Eagerly subscribe to sync set `entry.currentValue` to what the observable returns, and keep the observable alive until the component unmounts.
26
+ entry_1.subscription = entry_1.observable.subscribe();
27
+ cache.set(observable, entry_1);
33
28
  }
34
- return [
35
- function getSnapshot() {
36
- // @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.
37
- return store.currentValue;
38
- },
39
- function subscribe(callback) {
40
- // @TODO: perf opt opportunity: we could do `store.subscription.unsubscribe()` here as we only need 1 subscription active to keep the observer alive
41
- var sub = store.observable.subscribe(callback);
42
- return function () {
43
- sub.unsubscribe();
44
- };
45
- },
46
- ];
47
- }, [observable]), getSnapshot = _a[0], subscribe = _a[1];
48
- var shouldRestoreSubscriptionRef = useRef(false);
49
- useEffect(function () {
50
- var store = getOrCreateStore(observable, initialValueRef.current);
51
- if (shouldRestoreSubscriptionRef.current) {
52
- if (store.subscription.closed) {
53
- store.subscription = store.observable.subscribe();
54
- }
55
- shouldRestoreSubscriptionRef.current = false;
29
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
30
+ var instance = cache.get(observable);
31
+ if (instance.subscription.closed) {
32
+ instance.subscription = instance.observable.subscribe();
56
33
  }
57
- return function () {
58
- // React StrictMode will call effects as `setup + teardown + setup` thus we can't trust this callback as "react is about to unmount"
59
- // 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
60
- shouldRestoreSubscriptionRef.current = !store.subscription.closed;
61
- store.subscription.unsubscribe();
34
+ return {
35
+ subscribe: function (onStoreChange) {
36
+ var subscription = instance.observable.subscribe(onStoreChange);
37
+ instance.subscription.unsubscribe();
38
+ return function () { return subscription.unsubscribe(); };
39
+ },
40
+ getSnapshot: function () { return instance.snapshot; },
62
41
  };
63
42
  }, [observable]);
64
- return useSyncExternalStore(subscribe, getSnapshot);
43
+ return useSyncExternalStore(store.subscribe, store.getSnapshot);
65
44
  }
@@ -1,3 +1,8 @@
1
1
  import { DependencyList } from 'react';
2
2
  import { Observable } from 'rxjs';
3
3
  export declare function useObservableCallback<T, U>(fn: (arg: Observable<T>) => Observable<U>, dependencies?: DependencyList): (arg: T) => void;
4
+ export declare function useObservableEvent<T, U>(fn: (arg: Observable<T>) => Observable<U>): (arg: T) => void;
5
+ /**
6
+ * This is a ponyfill of the upcoming `useEffectEvent` hook that'll arrive in React 19
7
+ */
8
+ export declare function useEffectEvent<const T extends (...args: any[]) => void>(fn: T): T;
@@ -1,5 +1,5 @@
1
1
  import { observableCallback } from 'observable-callback';
2
- import { useCallback, useEffect, useRef } from 'react';
2
+ import { useCallback, useEffect, useInsertionEffect, useRef, useState } from 'react';
3
3
  var EMPTY_DEPS = [];
4
4
  export function useObservableCallback(fn, dependencies) {
5
5
  if (dependencies === void 0) { dependencies = EMPTY_DEPS; }
@@ -17,3 +17,30 @@ export function useObservableCallback(fn, dependencies) {
17
17
  }, [calls$, call, callback]);
18
18
  return call;
19
19
  }
20
+ export function useObservableEvent(fn) {
21
+ var _a = useState(function () { return observableCallback(); })[0], calls$ = _a[0], call = _a[1];
22
+ var callback = useEffectEvent(fn);
23
+ useEffect(function () {
24
+ var subscription = calls$.pipe(callback).subscribe();
25
+ return function () { return subscription.unsubscribe(); };
26
+ }, [callback, calls$]);
27
+ return call;
28
+ }
29
+ /**
30
+ * This is a ponyfill of the upcoming `useEffectEvent` hook that'll arrive in React 19
31
+ */
32
+ export function useEffectEvent(fn) {
33
+ var ref = useRef(null);
34
+ useInsertionEffect(function () {
35
+ ref.current = fn;
36
+ }, [fn]);
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ return useCallback(function () {
39
+ var args = [];
40
+ for (var _i = 0; _i < arguments.length; _i++) {
41
+ args[_i] = arguments[_i];
42
+ }
43
+ var f = ref.current;
44
+ return f.apply(void 0, args);
45
+ }, []);
46
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-rx",
3
- "version": "3.0.0-2",
3
+ "version": "3.0.0-4",
4
4
  "description": "React + RxJS = <3",
5
5
  "keywords": [
6
6
  "action",
@@ -69,39 +69,40 @@
69
69
  "test": "jest",
70
70
  "lint": "eslint --cache ."
71
71
  },
72
+ "prettier": "@sanity/prettier-config",
72
73
  "dependencies": {
73
74
  "observable-callback": "^1.0.3"
74
75
  },
75
76
  "devDependencies": {
77
+ "@sanity/prettier-config": "^1.0.2",
76
78
  "@sanity/semantic-release-preset": "^2.0.5",
77
- "@testing-library/dom": "^8.20.1",
78
- "@testing-library/react": "^13.4.0",
79
+ "@testing-library/dom": "^10.1.0",
80
+ "@testing-library/react": "^16.0.0",
79
81
  "@types/jest": "^29.5.3",
80
82
  "@types/node": "^18.17.5",
81
83
  "@types/react": "^18.3.3",
82
84
  "@types/react-dom": "^18.3.0",
83
- "@typescript-eslint/eslint-plugin": "7.12.0",
84
- "@typescript-eslint/parser": "7.12.0",
85
- "eslint": "8.57.0",
86
- "eslint-config-prettier": "9.1.0",
87
- "eslint-plugin-prettier": "5.1.3",
88
- "eslint-plugin-react": "7.34.2",
89
- "eslint-plugin-react-compiler": "^0.0.0-experimental-51a85ea-20240601",
90
- "eslint-plugin-react-hooks": "4.6.2",
85
+ "@typescript-eslint/eslint-plugin": "^7.12.0",
86
+ "@typescript-eslint/parser": "^7.12.0",
87
+ "eslint": "^8.57.0",
88
+ "eslint-config-prettier": "^9.1.0",
89
+ "eslint-plugin-prettier": "^5.1.3",
90
+ "eslint-plugin-react": "^7.34.2",
91
+ "eslint-plugin-react-compiler": "0.0.0-experimental-51a85ea-20240601",
92
+ "eslint-plugin-react-hooks": "^4.6.2",
91
93
  "eslint-plugin-simple-import-sort": "^12.1.0",
92
94
  "jest": "^29.6.2",
93
95
  "jest-environment-jsdom": "^29.6.2",
94
96
  "jsdom": "^20.0.3",
95
97
  "npm-run-all": "^4.1.5",
96
- "prettier": "^2.8.8",
97
- "prettier-plugin-packagejson": "^2.4.5",
98
+ "prettier": "^3.3.1",
98
99
  "react": "^18.3.1",
99
100
  "react-dom": "^18.3.1",
100
101
  "react-test-renderer": "^18.3.1",
101
102
  "rimraf": "^3.0.2",
102
103
  "rxjs": "^7.8.1",
103
104
  "ts-jest": "^29.1.1",
104
- "typescript": "4.7.4"
105
+ "typescript": "5.4.5"
105
106
  },
106
107
  "peerDependencies": {
107
108
  "react": "^18.3 || >=19.0.0-rc",
@@ -4,7 +4,7 @@ import {BehaviorSubject, Observable} from 'rxjs'
4
4
 
5
5
  import {useObservable} from '../useObservable'
6
6
 
7
- const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
7
+ const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
8
8
 
9
9
  // NOTE: Jest runs NODE_ENV=test by default, which enables development flags for React
10
10
 
@@ -1,33 +1,17 @@
1
1
  import {useEffect, useMemo, useRef, useSyncExternalStore} from 'react'
2
- import {Observable, Subscription} from 'rxjs'
2
+ import type {Observable, Subscription} from 'rxjs'
3
3
  import {shareReplay, tap} from 'rxjs/operators'
4
4
 
5
5
  function getValue<T>(value: T): T extends () => infer U ? U : T {
6
6
  return typeof value === 'function' ? value() : value
7
7
  }
8
-
9
8
  interface CacheRecord<T> {
10
9
  subscription: Subscription
11
10
  observable: Observable<T>
12
- currentValue: T
11
+ snapshot: T
13
12
  }
14
13
 
15
14
  const cache = new WeakMap<Observable<any>, CacheRecord<any>>()
16
- function getOrCreateStore<T>(inputObservable: Observable<T>, initialValue: T) {
17
- if (!cache.has(inputObservable)) {
18
- const entry: Partial<CacheRecord<T>> = {currentValue: initialValue}
19
- entry.observable = inputObservable.pipe(
20
- shareReplay({refCount: true, bufferSize: 1}),
21
- tap(value => (entry.currentValue = value)),
22
- )
23
-
24
- // Eagerly subscribe to sync set `entry.currentValue` to what the observable returns
25
- entry.subscription = entry.observable.subscribe()
26
-
27
- cache.set(inputObservable, entry as CacheRecord<T>)
28
- }
29
- return cache.get(inputObservable)!
30
- }
31
15
 
32
16
  export function useObservable<ObservableType extends Observable<any>>(
33
17
  observable: ObservableType,
@@ -37,56 +21,47 @@ export function useObservable<ObservableType extends Observable<any>>(
37
21
  * Store the initialValue in a ref, as we don't want a changed `initialValue` to trigger a re-subscription.
38
22
  * But we also don't want the initialValue to be stale if the observable changes.
39
23
  */
40
- const initialValueRef = useRef(getValue(initialValue))
24
+ const initialValueRef = useRef(getValue(initialValue) as UnboxObservable<ObservableType>)
41
25
 
42
26
  /**
43
27
  * Ensures that the initialValue is always up-to-date in case the observable changes.
44
28
  */
45
29
  useEffect(() => {
46
- initialValueRef.current = getValue(initialValue)
30
+ initialValueRef.current = getValue(initialValue) as UnboxObservable<ObservableType>
47
31
  }, [initialValue])
48
32
 
49
- const [getSnapshot, subscribe] = useMemo<
50
- [() => UnboxObservable<ObservableType>, Parameters<typeof useSyncExternalStore>[0]]
51
- >(() => {
52
- const store = getOrCreateStore(observable, initialValueRef.current)
53
- if (store.subscription.closed) {
54
- store.subscription = store.observable.subscribe()
55
- }
56
- return [
57
- function getSnapshot() {
58
- // @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.
59
- return store.currentValue
60
- },
61
- function subscribe(callback: () => void) {
62
- // @TODO: perf opt opportunity: we could do `store.subscription.unsubscribe()` here as we only need 1 subscription active to keep the observer alive
63
- const sub = store.observable.subscribe(callback)
64
- return () => {
65
- sub.unsubscribe()
66
- }
67
- },
68
- ]
69
- }, [observable])
70
-
71
- const shouldRestoreSubscriptionRef = useRef(false)
72
- useEffect(() => {
73
- const store = getOrCreateStore(observable, initialValueRef.current)
74
- if (shouldRestoreSubscriptionRef.current) {
75
- if (store.subscription.closed) {
76
- store.subscription = store.observable.subscribe()
33
+ const store = useMemo(() => {
34
+ if (!cache.has(observable)) {
35
+ const entry: Partial<CacheRecord<UnboxObservable<ObservableType>>> = {
36
+ snapshot: initialValueRef.current,
77
37
  }
78
- shouldRestoreSubscriptionRef.current = false
38
+ entry.observable = observable.pipe(
39
+ shareReplay({refCount: true, bufferSize: 1}),
40
+ tap((value) => (entry.snapshot = value)),
41
+ )
42
+
43
+ // Eagerly subscribe to sync set `entry.currentValue` to what the observable returns, and keep the observable alive until the component unmounts.
44
+ entry.subscription = entry.observable.subscribe()
45
+
46
+ cache.set(observable, entry as CacheRecord<UnboxObservable<ObservableType>>)
47
+ }
48
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
49
+ const instance = cache.get(observable)!
50
+ if (instance.subscription.closed) {
51
+ instance.subscription = instance.observable.subscribe()
79
52
  }
80
53
 
81
- return () => {
82
- // React StrictMode will call effects as `setup + teardown + setup` thus we can't trust this callback as "react is about to unmount"
83
- // 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
84
- shouldRestoreSubscriptionRef.current = !store.subscription.closed
85
- store.subscription.unsubscribe()
54
+ return {
55
+ subscribe: (onStoreChange: () => void) => {
56
+ const subscription = instance.observable.subscribe(onStoreChange)
57
+ instance.subscription.unsubscribe()
58
+ return () => subscription.unsubscribe()
59
+ },
60
+ getSnapshot: () => instance.snapshot,
86
61
  }
87
62
  }, [observable])
88
63
 
89
- return useSyncExternalStore<UnboxObservable<ObservableType>>(subscribe, getSnapshot)
64
+ return useSyncExternalStore<UnboxObservable<ObservableType>>(store.subscribe, store.getSnapshot)
90
65
  }
91
66
 
92
67
  type UnboxObservable<T> = T extends Observable<infer U> ? U : never
@@ -1,5 +1,5 @@
1
1
  import {observableCallback} from 'observable-callback'
2
- import {DependencyList, useCallback, useEffect, useRef} from 'react'
2
+ import {DependencyList, useCallback, useEffect, useInsertionEffect, useRef, useState} from 'react'
3
3
  import {Observable} from 'rxjs'
4
4
 
5
5
  const EMPTY_DEPS: DependencyList = []
@@ -27,3 +27,38 @@ export function useObservableCallback<T, U>(
27
27
 
28
28
  return call
29
29
  }
30
+
31
+ export function useObservableEvent<T, U>(
32
+ fn: (arg: Observable<T>) => Observable<U>,
33
+ ): (arg: T) => void {
34
+ const [[calls$, call]] = useState(() => observableCallback<T>())
35
+
36
+ const callback = useEffectEvent(fn)
37
+
38
+ useEffect(() => {
39
+ const subscription = calls$.pipe(callback).subscribe()
40
+ return () => subscription.unsubscribe()
41
+ }, [callback, calls$])
42
+
43
+ return call
44
+ }
45
+
46
+ /**
47
+ * This is a ponyfill of the upcoming `useEffectEvent` hook that'll arrive in React 19
48
+ */
49
+ export function useEffectEvent<
50
+ const T extends (
51
+ ...args: // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
+ any[]
53
+ ) => void,
54
+ >(fn: T): T {
55
+ const ref = useRef<T | null>(null)
56
+ useInsertionEffect(() => {
57
+ ref.current = fn
58
+ }, [fn])
59
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
60
+ return useCallback((...args: any[]) => {
61
+ const f = ref.current!
62
+ return f(...args)
63
+ }, []) as T
64
+ }