@slimr/react 3.0.69 → 3.0.71

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/README.md CHANGED
@@ -46,6 +46,24 @@ const MyComponent = forwardRef((props, ref1) => {
46
46
  })
47
47
  ```
48
48
 
49
+ ### Observable
50
+
51
+ Provides a variable with observable capabilities, like pub/sub and React hook integration.
52
+
53
+ ```tsx
54
+ import { Observable } from '@slimr/react';
55
+ const myObservable = new Observable('myObservable', 0);
56
+ myObservable.subscribe((newValue) => { console.log('New value:', newValue); });
57
+ setTimeout(() => { myObservable.val = 42; }, 1000);
58
+ setTimeout(() => { myObservable.set(50); }, 2000);
59
+ setTimeout(() => { myObservable.set(last => last + 1); }, 3000);
60
+
61
+ function MyComponent() {
62
+ myObservable.use();
63
+ return <div>{myObservable.value}</div>;
64
+ }
65
+ ```
66
+
49
67
  ### useDeepCompareMemo and useShallowCompareMemo
50
68
 
51
69
  like react-use's useDeepEffects, but for memos
@@ -0,0 +1,60 @@
1
+ type ObservableSetter<T> = (value: T) => T;
2
+ /**
3
+ * Provides a variable with observable capabilities, like pub/sub and React hook integration.
4
+ *
5
+ * @usage
6
+ * ```tsx
7
+ * const myObservable = new Observable('myObservable', 0);
8
+ * myObservable.subscribe((newValue) => { console.log('New value:', newValue); });
9
+ * setTimeout(() => { myObservable.val = 42; }, 1000);
10
+ * setTimeout(() => { myObservable.set(50); }, 2000);
11
+ * setTimeout(() => { myObservable.set(last => last + 1); }, 1000);
12
+ *
13
+ * function MyComponent() {
14
+ * myObservable.use();
15
+ * return <div>{myObservable.value}</div>;
16
+ * }
17
+ * ```
18
+ */
19
+ export declare class Observable<T> {
20
+ name: string;
21
+ private _value;
22
+ private subscribers;
23
+ constructor(name: string, initialValue: T);
24
+ publish(): Promise<void[]>;
25
+ /**
26
+ * Sets a new value for the observable and notifies subscribers if the value changed.
27
+ *
28
+ * @param newValueOrSetter - The new value or a function that takes the current value and returns the new value.
29
+ * @returns The new value.
30
+ */
31
+ set(setter: ObservableSetter<T>): Promise<void>;
32
+ set(newValue: T): Promise<void>;
33
+ /**
34
+ * Register a callback to be called when the observable value changes.
35
+ * @param cb - The callback to be called when the value changes.
36
+ * @returns
37
+ */
38
+ subscribe(cb: (value: T) => void): () => void;
39
+ unsubscribe(cb: (value: T) => void): void;
40
+ /**
41
+ * A React hook to use this observable in a component
42
+ *
43
+ * It does not return the value, but subscribes to changes and forces re-render
44
+ * when the value changes.
45
+ *
46
+ * @usage
47
+ * ```tsx
48
+ * const myObservable = new Observable('myObservable', 0);
49
+ *
50
+ * function MyComponent() {
51
+ * myObservable.use();
52
+ * return <div>{myObservable.value}</div>;
53
+ * }
54
+ * ```
55
+ */
56
+ use(): void;
57
+ get val(): T;
58
+ set val(newValue: T);
59
+ }
60
+ export {};
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ // this file has exports to create an observable and useObservable react hook
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.Observable = void 0;
5
+ const util_1 = require("@slimr/util");
6
+ const react_1 = require("react");
7
+ // @ts-expect-error
8
+ globalThis.observables = {};
9
+ /**
10
+ * Provides a variable with observable capabilities, like pub/sub and React hook integration.
11
+ *
12
+ * @usage
13
+ * ```tsx
14
+ * const myObservable = new Observable('myObservable', 0);
15
+ * myObservable.subscribe((newValue) => { console.log('New value:', newValue); });
16
+ * setTimeout(() => { myObservable.val = 42; }, 1000);
17
+ * setTimeout(() => { myObservable.set(50); }, 2000);
18
+ * setTimeout(() => { myObservable.set(last => last + 1); }, 1000);
19
+ *
20
+ * function MyComponent() {
21
+ * myObservable.use();
22
+ * return <div>{myObservable.value}</div>;
23
+ * }
24
+ * ```
25
+ */
26
+ class Observable {
27
+ name;
28
+ _value;
29
+ subscribers;
30
+ constructor(name, initialValue) {
31
+ this.name = name;
32
+ this._value = initialValue;
33
+ this.subscribers = new Set();
34
+ // @ts-expect-error
35
+ globalThis.observables[name] = this;
36
+ }
37
+ async publish() {
38
+ return Promise.all([...this.subscribers].map(subscriber => subscriber(this._value)));
39
+ }
40
+ async set(newValueOrSetter) {
41
+ const newValue = typeof newValueOrSetter === 'function'
42
+ ? newValueOrSetter(this._value)
43
+ : newValueOrSetter;
44
+ if (!(0, util_1.areEqualDeep)(this._value, newValue)) {
45
+ this._value = newValue;
46
+ await this.publish();
47
+ }
48
+ }
49
+ /**
50
+ * Register a callback to be called when the observable value changes.
51
+ * @param cb - The callback to be called when the value changes.
52
+ * @returns
53
+ */
54
+ subscribe(cb) {
55
+ // promisify the callback so it behaves like an async function even if it's not
56
+ // so that we can await all subscribers in publish()
57
+ const cbPromise = async (newValue) => cb(newValue);
58
+ this.subscribers.add(cbPromise);
59
+ return () => this.unsubscribe(cb);
60
+ }
61
+ unsubscribe(cb) {
62
+ this.subscribers.delete(cb);
63
+ }
64
+ /**
65
+ * A React hook to use this observable in a component
66
+ *
67
+ * It does not return the value, but subscribes to changes and forces re-render
68
+ * when the value changes.
69
+ *
70
+ * @usage
71
+ * ```tsx
72
+ * const myObservable = new Observable('myObservable', 0);
73
+ *
74
+ * function MyComponent() {
75
+ * myObservable.use();
76
+ * return <div>{myObservable.value}</div>;
77
+ * }
78
+ * ```
79
+ */
80
+ use() {
81
+ const [_, setValue] = (0, react_1.useState)(this.val);
82
+ (0, react_1.useEffect)(() => this.subscribe(setValue), []);
83
+ }
84
+ get val() {
85
+ // Freeze the value to avoid cases where the internal object may be mutated
86
+ // without triggering subscribers.
87
+ return Object.freeze(this._value);
88
+ }
89
+ set val(newValue) {
90
+ this.set(newValue);
91
+ }
92
+ }
93
+ exports.Observable = Observable;
94
+ //# sourceMappingURL=Observable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Observable.js","sourceRoot":"","sources":["../src/Observable.ts"],"names":[],"mappings":";AAAA,6EAA6E;;;AAE7E,sCAA2C;AAC3C,iCAA4C;AAI5C,mBAAmB;AACnB,UAAU,CAAC,WAAW,GAAG,EAAE,CAAC;AAE5B;;;;;;;;;;;;;;;;GAgBG;AACH,MAAa,UAAU;IACrB,IAAI,CAAS;IACL,MAAM,CAAI;IACV,WAAW,CAA0B;IAE7C,YAAY,IAAY,EAAE,YAAe;QACvC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,MAAM,GAAG,YAAY,CAAC;QAC3B,IAAI,CAAC,WAAW,GAAG,IAAI,GAAG,EAAE,CAAC;QAC7B,mBAAmB;QACnB,UAAU,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACtC,CAAC;IAED,KAAK,CAAC,OAAO;QACX,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACvF,CAAC;IAUD,KAAK,CAAC,GAAG,CAAC,gBAAqB;QAC7B,MAAM,QAAQ,GACZ,OAAO,gBAAgB,KAAK,UAAU;YACpC,CAAC,CAAE,gBAAwC,CAAC,IAAI,CAAC,MAAM,CAAC;YACxD,CAAC,CAAC,gBAAgB,CAAC;QACvB,IAAI,CAAC,IAAA,mBAAY,EAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;YACzC,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC;YACvB,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QACvB,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,SAAS,CAAC,EAAsB;QAC9B,+EAA+E;QAC/E,oDAAoD;QACpD,MAAM,SAAS,GAAG,KAAK,EAAE,QAAW,EAAE,EAAE,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC;QACtD,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAChC,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;IACpC,CAAC;IAED,WAAW,CAAC,EAAsB;QAChC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC9B,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IACH,GAAG;QACD,MAAM,CAAC,CAAC,EAAE,QAAQ,CAAC,GAAG,IAAA,gBAAQ,EAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACzC,IAAA,iBAAS,EAAC,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAA;IAC/C,CAAC;IAED,IAAI,GAAG;QACL,2EAA2E;QAC3E,kCAAkC;QAClC,OAAO,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACpC,CAAC;IACD,IAAI,GAAG,CAAC,QAAW;QACjB,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACrB,CAAC;CACF;AAlFD,gCAkFC"}
@@ -0,0 +1,110 @@
1
+ // this file has exports to create an observable and useObservable react hook
2
+
3
+ import { areEqualDeep } from '@slimr/util';
4
+ import { useState, useEffect } from 'react';
5
+
6
+ type ObservableSetter<T> = (value: T) => T;
7
+
8
+ // @ts-expect-error
9
+ globalThis.observables = {};
10
+
11
+ /**
12
+ * Provides a variable with observable capabilities, like pub/sub and React hook integration.
13
+ *
14
+ * @usage
15
+ * ```tsx
16
+ * const myObservable = new Observable('myObservable', 0);
17
+ * myObservable.subscribe((newValue) => { console.log('New value:', newValue); });
18
+ * setTimeout(() => { myObservable.val = 42; }, 1000);
19
+ * setTimeout(() => { myObservable.set(50); }, 2000);
20
+ * setTimeout(() => { myObservable.set(last => last + 1); }, 1000);
21
+ *
22
+ * function MyComponent() {
23
+ * myObservable.use();
24
+ * return <div>{myObservable.value}</div>;
25
+ * }
26
+ * ```
27
+ */
28
+ export class Observable<T> {
29
+ name: string;
30
+ private _value: T;
31
+ private subscribers: Set<(value: T) => void>;
32
+
33
+ constructor(name: string, initialValue: T) {
34
+ this.name = name;
35
+ this._value = initialValue;
36
+ this.subscribers = new Set();
37
+ // @ts-expect-error
38
+ globalThis.observables[name] = this;
39
+ }
40
+
41
+ async publish() {
42
+ return Promise.all([...this.subscribers].map(subscriber => subscriber(this._value)));
43
+ }
44
+
45
+ /**
46
+ * Sets a new value for the observable and notifies subscribers if the value changed.
47
+ *
48
+ * @param newValueOrSetter - The new value or a function that takes the current value and returns the new value.
49
+ * @returns The new value.
50
+ */
51
+ async set(setter: ObservableSetter<T>): Promise<void>;
52
+ async set(newValue: T): Promise<void>;
53
+ async set(newValueOrSetter: any): Promise<void> {
54
+ const newValue =
55
+ typeof newValueOrSetter === 'function'
56
+ ? (newValueOrSetter as ObservableSetter<T>)(this._value)
57
+ : newValueOrSetter;
58
+ if (!areEqualDeep(this._value, newValue)) {
59
+ this._value = newValue;
60
+ await this.publish();
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Register a callback to be called when the observable value changes.
66
+ * @param cb - The callback to be called when the value changes.
67
+ * @returns
68
+ */
69
+ subscribe(cb: (value: T) => void): () => void {
70
+ // promisify the callback so it behaves like an async function even if it's not
71
+ // so that we can await all subscribers in publish()
72
+ const cbPromise = async (newValue: T) => cb(newValue);
73
+ this.subscribers.add(cbPromise);
74
+ return () => this.unsubscribe(cb);
75
+ }
76
+
77
+ unsubscribe(cb: (value: T) => void): void {
78
+ this.subscribers.delete(cb);
79
+ }
80
+
81
+ /**
82
+ * A React hook to use this observable in a component
83
+ *
84
+ * It does not return the value, but subscribes to changes and forces re-render
85
+ * when the value changes.
86
+ *
87
+ * @usage
88
+ * ```tsx
89
+ * const myObservable = new Observable('myObservable', 0);
90
+ *
91
+ * function MyComponent() {
92
+ * myObservable.use();
93
+ * return <div>{myObservable.value}</div>;
94
+ * }
95
+ * ```
96
+ */
97
+ use() {
98
+ const [_, setValue] = useState(this.val);
99
+ useEffect(() => this.subscribe(setValue), [])
100
+ }
101
+
102
+ get val(): T {
103
+ // Freeze the value to avoid cases where the internal object may be mutated
104
+ // without triggering subscribers.
105
+ return Object.freeze(this._value);
106
+ }
107
+ set val(newValue: T) {
108
+ this.set(newValue);
109
+ }
110
+ }
package/cjs/index.d.ts CHANGED
@@ -7,4 +7,5 @@ export * from "react-use";
7
7
  export * from "./merge-refs.js";
8
8
  export * from "./useColorScheme.js";
9
9
  export * from "./useMemos.js";
10
+ export * from "./Observable.js";
10
11
  export * from "./useSet2.js";
package/cjs/index.js CHANGED
@@ -23,5 +23,6 @@ __exportStar(require("react-use"), exports);
23
23
  __exportStar(require("./merge-refs.js"), exports);
24
24
  __exportStar(require("./useColorScheme.js"), exports);
25
25
  __exportStar(require("./useMemos.js"), exports);
26
+ __exportStar(require("./Observable.js"), exports);
26
27
  __exportStar(require("./useSet2.js"), exports);
27
28
  //# sourceMappingURL=index.js.map
package/cjs/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,+CAA4B;AAC5B,kDAA+B;AAC/B,gDAA6B;AAC7B,gDAA6B;AAC7B,6CAA0B;AAC1B,4CAAyB;AAEzB,kDAA+B;AAC/B,sDAAmC;AACnC,gDAA6B;AAC7B,+CAA4B"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,+CAA4B;AAC5B,kDAA+B;AAC/B,gDAA6B;AAC7B,gDAA6B;AAC7B,6CAA0B;AAC1B,4CAAyB;AAEzB,kDAA+B;AAC/B,sDAAmC;AACnC,gDAA6B;AAC7B,kDAA+B;AAC/B,+CAA4B"}
package/cjs/index.ts CHANGED
@@ -8,4 +8,5 @@ export * from "react-use"
8
8
  export * from "./merge-refs.js"
9
9
  export * from "./useColorScheme.js"
10
10
  export * from "./useMemos.js"
11
+ export * from "./Observable.js"
11
12
  export * from "./useSet2.js"
@@ -0,0 +1,60 @@
1
+ type ObservableSetter<T> = (value: T) => T;
2
+ /**
3
+ * Provides a variable with observable capabilities, like pub/sub and React hook integration.
4
+ *
5
+ * @usage
6
+ * ```tsx
7
+ * const myObservable = new Observable('myObservable', 0);
8
+ * myObservable.subscribe((newValue) => { console.log('New value:', newValue); });
9
+ * setTimeout(() => { myObservable.val = 42; }, 1000);
10
+ * setTimeout(() => { myObservable.set(50); }, 2000);
11
+ * setTimeout(() => { myObservable.set(last => last + 1); }, 1000);
12
+ *
13
+ * function MyComponent() {
14
+ * myObservable.use();
15
+ * return <div>{myObservable.value}</div>;
16
+ * }
17
+ * ```
18
+ */
19
+ export declare class Observable<T> {
20
+ name: string;
21
+ private _value;
22
+ private subscribers;
23
+ constructor(name: string, initialValue: T);
24
+ publish(): Promise<void[]>;
25
+ /**
26
+ * Sets a new value for the observable and notifies subscribers if the value changed.
27
+ *
28
+ * @param newValueOrSetter - The new value or a function that takes the current value and returns the new value.
29
+ * @returns The new value.
30
+ */
31
+ set(setter: ObservableSetter<T>): Promise<void>;
32
+ set(newValue: T): Promise<void>;
33
+ /**
34
+ * Register a callback to be called when the observable value changes.
35
+ * @param cb - The callback to be called when the value changes.
36
+ * @returns
37
+ */
38
+ subscribe(cb: (value: T) => void): () => void;
39
+ unsubscribe(cb: (value: T) => void): void;
40
+ /**
41
+ * A React hook to use this observable in a component
42
+ *
43
+ * It does not return the value, but subscribes to changes and forces re-render
44
+ * when the value changes.
45
+ *
46
+ * @usage
47
+ * ```tsx
48
+ * const myObservable = new Observable('myObservable', 0);
49
+ *
50
+ * function MyComponent() {
51
+ * myObservable.use();
52
+ * return <div>{myObservable.value}</div>;
53
+ * }
54
+ * ```
55
+ */
56
+ use(): void;
57
+ get val(): T;
58
+ set val(newValue: T);
59
+ }
60
+ export {};
@@ -0,0 +1,90 @@
1
+ // this file has exports to create an observable and useObservable react hook
2
+ import { areEqualDeep } from '@slimr/util';
3
+ import { useState, useEffect } from 'react';
4
+ // @ts-expect-error
5
+ globalThis.observables = {};
6
+ /**
7
+ * Provides a variable with observable capabilities, like pub/sub and React hook integration.
8
+ *
9
+ * @usage
10
+ * ```tsx
11
+ * const myObservable = new Observable('myObservable', 0);
12
+ * myObservable.subscribe((newValue) => { console.log('New value:', newValue); });
13
+ * setTimeout(() => { myObservable.val = 42; }, 1000);
14
+ * setTimeout(() => { myObservable.set(50); }, 2000);
15
+ * setTimeout(() => { myObservable.set(last => last + 1); }, 1000);
16
+ *
17
+ * function MyComponent() {
18
+ * myObservable.use();
19
+ * return <div>{myObservable.value}</div>;
20
+ * }
21
+ * ```
22
+ */
23
+ export class Observable {
24
+ name;
25
+ _value;
26
+ subscribers;
27
+ constructor(name, initialValue) {
28
+ this.name = name;
29
+ this._value = initialValue;
30
+ this.subscribers = new Set();
31
+ // @ts-expect-error
32
+ globalThis.observables[name] = this;
33
+ }
34
+ async publish() {
35
+ return Promise.all([...this.subscribers].map(subscriber => subscriber(this._value)));
36
+ }
37
+ async set(newValueOrSetter) {
38
+ const newValue = typeof newValueOrSetter === 'function'
39
+ ? newValueOrSetter(this._value)
40
+ : newValueOrSetter;
41
+ if (!areEqualDeep(this._value, newValue)) {
42
+ this._value = newValue;
43
+ await this.publish();
44
+ }
45
+ }
46
+ /**
47
+ * Register a callback to be called when the observable value changes.
48
+ * @param cb - The callback to be called when the value changes.
49
+ * @returns
50
+ */
51
+ subscribe(cb) {
52
+ // promisify the callback so it behaves like an async function even if it's not
53
+ // so that we can await all subscribers in publish()
54
+ const cbPromise = async (newValue) => cb(newValue);
55
+ this.subscribers.add(cbPromise);
56
+ return () => this.unsubscribe(cb);
57
+ }
58
+ unsubscribe(cb) {
59
+ this.subscribers.delete(cb);
60
+ }
61
+ /**
62
+ * A React hook to use this observable in a component
63
+ *
64
+ * It does not return the value, but subscribes to changes and forces re-render
65
+ * when the value changes.
66
+ *
67
+ * @usage
68
+ * ```tsx
69
+ * const myObservable = new Observable('myObservable', 0);
70
+ *
71
+ * function MyComponent() {
72
+ * myObservable.use();
73
+ * return <div>{myObservable.value}</div>;
74
+ * }
75
+ * ```
76
+ */
77
+ use() {
78
+ const [_, setValue] = useState(this.val);
79
+ useEffect(() => this.subscribe(setValue), []);
80
+ }
81
+ get val() {
82
+ // Freeze the value to avoid cases where the internal object may be mutated
83
+ // without triggering subscribers.
84
+ return Object.freeze(this._value);
85
+ }
86
+ set val(newValue) {
87
+ this.set(newValue);
88
+ }
89
+ }
90
+ //# sourceMappingURL=Observable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Observable.js","sourceRoot":"","sources":["../src/Observable.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAE7E,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAI5C,mBAAmB;AACnB,UAAU,CAAC,WAAW,GAAG,EAAE,CAAC;AAE5B;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,OAAO,UAAU;IACrB,IAAI,CAAS;IACL,MAAM,CAAI;IACV,WAAW,CAA0B;IAE7C,YAAY,IAAY,EAAE,YAAe;QACvC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,MAAM,GAAG,YAAY,CAAC;QAC3B,IAAI,CAAC,WAAW,GAAG,IAAI,GAAG,EAAE,CAAC;QAC7B,mBAAmB;QACnB,UAAU,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACtC,CAAC;IAED,KAAK,CAAC,OAAO;QACX,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACvF,CAAC;IAUD,KAAK,CAAC,GAAG,CAAC,gBAAqB;QAC7B,MAAM,QAAQ,GACZ,OAAO,gBAAgB,KAAK,UAAU;YACpC,CAAC,CAAE,gBAAwC,CAAC,IAAI,CAAC,MAAM,CAAC;YACxD,CAAC,CAAC,gBAAgB,CAAC;QACvB,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;YACzC,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC;YACvB,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QACvB,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,SAAS,CAAC,EAAsB;QAC9B,+EAA+E;QAC/E,oDAAoD;QACpD,MAAM,SAAS,GAAG,KAAK,EAAE,QAAW,EAAE,EAAE,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC;QACtD,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAChC,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;IACpC,CAAC;IAED,WAAW,CAAC,EAAsB;QAChC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC9B,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IACH,GAAG;QACD,MAAM,CAAC,CAAC,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACzC,SAAS,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAA;IAC/C,CAAC;IAED,IAAI,GAAG;QACL,2EAA2E;QAC3E,kCAAkC;QAClC,OAAO,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACpC,CAAC;IACD,IAAI,GAAG,CAAC,QAAW;QACjB,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACrB,CAAC;CACF"}
@@ -0,0 +1,110 @@
1
+ // this file has exports to create an observable and useObservable react hook
2
+
3
+ import { areEqualDeep } from '@slimr/util';
4
+ import { useState, useEffect } from 'react';
5
+
6
+ type ObservableSetter<T> = (value: T) => T;
7
+
8
+ // @ts-expect-error
9
+ globalThis.observables = {};
10
+
11
+ /**
12
+ * Provides a variable with observable capabilities, like pub/sub and React hook integration.
13
+ *
14
+ * @usage
15
+ * ```tsx
16
+ * const myObservable = new Observable('myObservable', 0);
17
+ * myObservable.subscribe((newValue) => { console.log('New value:', newValue); });
18
+ * setTimeout(() => { myObservable.val = 42; }, 1000);
19
+ * setTimeout(() => { myObservable.set(50); }, 2000);
20
+ * setTimeout(() => { myObservable.set(last => last + 1); }, 1000);
21
+ *
22
+ * function MyComponent() {
23
+ * myObservable.use();
24
+ * return <div>{myObservable.value}</div>;
25
+ * }
26
+ * ```
27
+ */
28
+ export class Observable<T> {
29
+ name: string;
30
+ private _value: T;
31
+ private subscribers: Set<(value: T) => void>;
32
+
33
+ constructor(name: string, initialValue: T) {
34
+ this.name = name;
35
+ this._value = initialValue;
36
+ this.subscribers = new Set();
37
+ // @ts-expect-error
38
+ globalThis.observables[name] = this;
39
+ }
40
+
41
+ async publish() {
42
+ return Promise.all([...this.subscribers].map(subscriber => subscriber(this._value)));
43
+ }
44
+
45
+ /**
46
+ * Sets a new value for the observable and notifies subscribers if the value changed.
47
+ *
48
+ * @param newValueOrSetter - The new value or a function that takes the current value and returns the new value.
49
+ * @returns The new value.
50
+ */
51
+ async set(setter: ObservableSetter<T>): Promise<void>;
52
+ async set(newValue: T): Promise<void>;
53
+ async set(newValueOrSetter: any): Promise<void> {
54
+ const newValue =
55
+ typeof newValueOrSetter === 'function'
56
+ ? (newValueOrSetter as ObservableSetter<T>)(this._value)
57
+ : newValueOrSetter;
58
+ if (!areEqualDeep(this._value, newValue)) {
59
+ this._value = newValue;
60
+ await this.publish();
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Register a callback to be called when the observable value changes.
66
+ * @param cb - The callback to be called when the value changes.
67
+ * @returns
68
+ */
69
+ subscribe(cb: (value: T) => void): () => void {
70
+ // promisify the callback so it behaves like an async function even if it's not
71
+ // so that we can await all subscribers in publish()
72
+ const cbPromise = async (newValue: T) => cb(newValue);
73
+ this.subscribers.add(cbPromise);
74
+ return () => this.unsubscribe(cb);
75
+ }
76
+
77
+ unsubscribe(cb: (value: T) => void): void {
78
+ this.subscribers.delete(cb);
79
+ }
80
+
81
+ /**
82
+ * A React hook to use this observable in a component
83
+ *
84
+ * It does not return the value, but subscribes to changes and forces re-render
85
+ * when the value changes.
86
+ *
87
+ * @usage
88
+ * ```tsx
89
+ * const myObservable = new Observable('myObservable', 0);
90
+ *
91
+ * function MyComponent() {
92
+ * myObservable.use();
93
+ * return <div>{myObservable.value}</div>;
94
+ * }
95
+ * ```
96
+ */
97
+ use() {
98
+ const [_, setValue] = useState(this.val);
99
+ useEffect(() => this.subscribe(setValue), [])
100
+ }
101
+
102
+ get val(): T {
103
+ // Freeze the value to avoid cases where the internal object may be mutated
104
+ // without triggering subscribers.
105
+ return Object.freeze(this._value);
106
+ }
107
+ set val(newValue: T) {
108
+ this.set(newValue);
109
+ }
110
+ }
package/esm/index.d.ts CHANGED
@@ -7,4 +7,5 @@ export * from "react-use";
7
7
  export * from "./merge-refs.js";
8
8
  export * from "./useColorScheme.js";
9
9
  export * from "./useMemos.js";
10
+ export * from "./Observable.js";
10
11
  export * from "./useSet2.js";
package/esm/index.js CHANGED
@@ -7,5 +7,6 @@ export * from "react-use";
7
7
  export * from "./merge-refs.js";
8
8
  export * from "./useColorScheme.js";
9
9
  export * from "./useMemos.js";
10
+ export * from "./Observable.js";
10
11
  export * from "./useSet2.js";
11
12
  //# sourceMappingURL=index.js.map
package/esm/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AAEzB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,qBAAqB,CAAA;AACnC,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AAEzB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,qBAAqB,CAAA;AACnC,cAAc,eAAe,CAAA;AAC7B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA"}
package/esm/index.ts CHANGED
@@ -8,4 +8,5 @@ export * from "react-use"
8
8
  export * from "./merge-refs.js"
9
9
  export * from "./useColorScheme.js"
10
10
  export * from "./useMemos.js"
11
+ export * from "./Observable.js"
11
12
  export * from "./useSet2.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slimr/react",
3
- "version": "3.0.69",
3
+ "version": "3.0.71",
4
4
  "author": "Brian Dombrowski",
5
5
  "license": "ISC",
6
6
  "private": false,
@@ -31,12 +31,12 @@
31
31
  "start": "nodemon -w src -e '*' -x 'npm run build && cd ../demo && npm start'"
32
32
  },
33
33
  "dependencies": {
34
- "@slimr/forms": "^5.0.43",
35
- "@slimr/markdown": "^2.1.85",
36
- "@slimr/router": "^2.1.71",
37
- "@slimr/styled": "^2.1.82",
38
- "@slimr/swr": "^2.1.70",
39
- "@slimr/util": "^3.2.71",
34
+ "@slimr/forms": "^5.0.45",
35
+ "@slimr/markdown": "^2.1.87",
36
+ "@slimr/router": "^2.1.73",
37
+ "@slimr/styled": "^2.1.84",
38
+ "@slimr/swr": "^2.1.72",
39
+ "@slimr/util": "^3.2.73",
40
40
  "react-use": "17"
41
41
  },
42
42
  "peerDependencies": {
@@ -0,0 +1,110 @@
1
+ // this file has exports to create an observable and useObservable react hook
2
+
3
+ import { areEqualDeep } from '@slimr/util';
4
+ import { useState, useEffect } from 'react';
5
+
6
+ type ObservableSetter<T> = (value: T) => T;
7
+
8
+ // @ts-expect-error
9
+ globalThis.observables = {};
10
+
11
+ /**
12
+ * Provides a variable with observable capabilities, like pub/sub and React hook integration.
13
+ *
14
+ * @usage
15
+ * ```tsx
16
+ * const myObservable = new Observable('myObservable', 0);
17
+ * myObservable.subscribe((newValue) => { console.log('New value:', newValue); });
18
+ * setTimeout(() => { myObservable.val = 42; }, 1000);
19
+ * setTimeout(() => { myObservable.set(50); }, 2000);
20
+ * setTimeout(() => { myObservable.set(last => last + 1); }, 1000);
21
+ *
22
+ * function MyComponent() {
23
+ * myObservable.use();
24
+ * return <div>{myObservable.value}</div>;
25
+ * }
26
+ * ```
27
+ */
28
+ export class Observable<T> {
29
+ name: string;
30
+ private _value: T;
31
+ private subscribers: Set<(value: T) => void>;
32
+
33
+ constructor(name: string, initialValue: T) {
34
+ this.name = name;
35
+ this._value = initialValue;
36
+ this.subscribers = new Set();
37
+ // @ts-expect-error
38
+ globalThis.observables[name] = this;
39
+ }
40
+
41
+ async publish() {
42
+ return Promise.all([...this.subscribers].map(subscriber => subscriber(this._value)));
43
+ }
44
+
45
+ /**
46
+ * Sets a new value for the observable and notifies subscribers if the value changed.
47
+ *
48
+ * @param newValueOrSetter - The new value or a function that takes the current value and returns the new value.
49
+ * @returns The new value.
50
+ */
51
+ async set(setter: ObservableSetter<T>): Promise<void>;
52
+ async set(newValue: T): Promise<void>;
53
+ async set(newValueOrSetter: any): Promise<void> {
54
+ const newValue =
55
+ typeof newValueOrSetter === 'function'
56
+ ? (newValueOrSetter as ObservableSetter<T>)(this._value)
57
+ : newValueOrSetter;
58
+ if (!areEqualDeep(this._value, newValue)) {
59
+ this._value = newValue;
60
+ await this.publish();
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Register a callback to be called when the observable value changes.
66
+ * @param cb - The callback to be called when the value changes.
67
+ * @returns
68
+ */
69
+ subscribe(cb: (value: T) => void): () => void {
70
+ // promisify the callback so it behaves like an async function even if it's not
71
+ // so that we can await all subscribers in publish()
72
+ const cbPromise = async (newValue: T) => cb(newValue);
73
+ this.subscribers.add(cbPromise);
74
+ return () => this.unsubscribe(cb);
75
+ }
76
+
77
+ unsubscribe(cb: (value: T) => void): void {
78
+ this.subscribers.delete(cb);
79
+ }
80
+
81
+ /**
82
+ * A React hook to use this observable in a component
83
+ *
84
+ * It does not return the value, but subscribes to changes and forces re-render
85
+ * when the value changes.
86
+ *
87
+ * @usage
88
+ * ```tsx
89
+ * const myObservable = new Observable('myObservable', 0);
90
+ *
91
+ * function MyComponent() {
92
+ * myObservable.use();
93
+ * return <div>{myObservable.value}</div>;
94
+ * }
95
+ * ```
96
+ */
97
+ use() {
98
+ const [_, setValue] = useState(this.val);
99
+ useEffect(() => this.subscribe(setValue), [])
100
+ }
101
+
102
+ get val(): T {
103
+ // Freeze the value to avoid cases where the internal object may be mutated
104
+ // without triggering subscribers.
105
+ return Object.freeze(this._value);
106
+ }
107
+ set val(newValue: T) {
108
+ this.set(newValue);
109
+ }
110
+ }
package/src/index.ts CHANGED
@@ -8,4 +8,5 @@ export * from "react-use"
8
8
  export * from "./merge-refs.js"
9
9
  export * from "./useColorScheme.js"
10
10
  export * from "./useMemos.js"
11
+ export * from "./Observable.js"
11
12
  export * from "./useSet2.js"