@montra-interactive/deepstate-react 0.1.2 → 0.1.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.
- package/dist/hooks.d.ts +40 -12
- package/dist/hooks.d.ts.map +1 -1
- package/dist/index.js +50 -9
- package/package.json +2 -2
- package/src/hooks.ts +174 -32
package/dist/hooks.d.ts
CHANGED
|
@@ -4,6 +4,12 @@ import type { Observable } from "rxjs";
|
|
|
4
4
|
* Works with deepstate nodes since they extend Observable.
|
|
5
5
|
*/
|
|
6
6
|
type ObservableValue<T> = T extends Observable<infer V> ? V : never;
|
|
7
|
+
/**
|
|
8
|
+
* Type for object of observables -> object of their values
|
|
9
|
+
*/
|
|
10
|
+
type ObservableObjectValues<T extends Record<string, Observable<unknown>>> = {
|
|
11
|
+
[K in keyof T]: ObservableValue<T[K]>;
|
|
12
|
+
};
|
|
7
13
|
/**
|
|
8
14
|
* Hook to subscribe to any Observable and get its current value.
|
|
9
15
|
* Re-renders the component whenever the observable emits a new value.
|
|
@@ -78,7 +84,7 @@ export declare function useObservable<T>(observable$: Observable<T>, getSnapshot
|
|
|
78
84
|
*/
|
|
79
85
|
export declare function useStateValue<T extends Observable<unknown>>(node: T): ObservableValue<T>;
|
|
80
86
|
/**
|
|
81
|
-
* Hook to derive a value from
|
|
87
|
+
* Hook to derive a value from one or more deepstate nodes with a selector function.
|
|
82
88
|
* Only re-renders when the derived value changes (using reference equality by default).
|
|
83
89
|
*
|
|
84
90
|
* Use this when you need to compute/transform a value from state.
|
|
@@ -86,12 +92,12 @@ export declare function useStateValue<T extends Observable<unknown>>(node: T): O
|
|
|
86
92
|
*
|
|
87
93
|
* Uses React 18's useSyncExternalStore for concurrent-mode safety.
|
|
88
94
|
*
|
|
89
|
-
* @param node - A deepstate node to select from
|
|
90
|
-
* @param selector - Function to derive a value from the node's value
|
|
95
|
+
* @param node - A deepstate node, array of nodes, or object of nodes to select from
|
|
96
|
+
* @param selector - Function to derive a value from the node's value(s)
|
|
91
97
|
* @param equalityFn - Optional custom equality function (default: Object.is)
|
|
92
98
|
* @returns The derived value
|
|
93
99
|
*
|
|
94
|
-
* @example
|
|
100
|
+
* @example Single node
|
|
95
101
|
* ```tsx
|
|
96
102
|
* import { state } from 'deepstate';
|
|
97
103
|
* import { useSelector } from 'deepstate-react';
|
|
@@ -101,7 +107,7 @@ export declare function useStateValue<T extends Observable<unknown>>(node: T): O
|
|
|
101
107
|
* items: [{ id: 1, price: 10 }, { id: 2, price: 20 }]
|
|
102
108
|
* });
|
|
103
109
|
*
|
|
104
|
-
* // Derive a computed value
|
|
110
|
+
* // Derive a computed value from a single node
|
|
105
111
|
* function FullName() {
|
|
106
112
|
* const fullName = useSelector(
|
|
107
113
|
* store.user,
|
|
@@ -109,17 +115,34 @@ export declare function useStateValue<T extends Observable<unknown>>(node: T): O
|
|
|
109
115
|
* );
|
|
110
116
|
* return <span>{fullName}</span>;
|
|
111
117
|
* }
|
|
118
|
+
* ```
|
|
112
119
|
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
120
|
+
* @example Multiple nodes (array form)
|
|
121
|
+
* ```tsx
|
|
122
|
+
* // Combine multiple nodes - receives values as tuple
|
|
123
|
+
* function Progress() {
|
|
124
|
+
* const percentage = useSelector(
|
|
125
|
+
* [store.completed, store.total],
|
|
126
|
+
* ([completed, total]) => total > 0 ? (completed / total) * 100 : 0
|
|
118
127
|
* );
|
|
119
|
-
* return <span>
|
|
128
|
+
* return <span>{percentage}%</span>;
|
|
120
129
|
* }
|
|
130
|
+
* ```
|
|
121
131
|
*
|
|
122
|
-
*
|
|
132
|
+
* @example Multiple nodes (object form)
|
|
133
|
+
* ```tsx
|
|
134
|
+
* // Combine multiple nodes - receives values as object
|
|
135
|
+
* function Progress() {
|
|
136
|
+
* const percentage = useSelector(
|
|
137
|
+
* { completed: store.completed, total: store.total },
|
|
138
|
+
* ({ completed, total }) => total > 0 ? (completed / total) * 100 : 0
|
|
139
|
+
* );
|
|
140
|
+
* return <span>{percentage}%</span>;
|
|
141
|
+
* }
|
|
142
|
+
* ```
|
|
143
|
+
*
|
|
144
|
+
* @example With custom equality
|
|
145
|
+
* ```tsx
|
|
123
146
|
* function ItemIds() {
|
|
124
147
|
* const ids = useSelector(
|
|
125
148
|
* store.items,
|
|
@@ -131,5 +154,10 @@ export declare function useStateValue<T extends Observable<unknown>>(node: T): O
|
|
|
131
154
|
* ```
|
|
132
155
|
*/
|
|
133
156
|
export declare function useSelector<T extends Observable<unknown>, R>(node: T, selector: (value: ObservableValue<T>) => R, equalityFn?: (a: R, b: R) => boolean): R;
|
|
157
|
+
export declare function useSelector<T1 extends Observable<unknown>, T2 extends Observable<unknown>, R>(nodes: [T1, T2], selector: (values: [ObservableValue<T1>, ObservableValue<T2>]) => R, equalityFn?: (a: R, b: R) => boolean): R;
|
|
158
|
+
export declare function useSelector<T1 extends Observable<unknown>, T2 extends Observable<unknown>, T3 extends Observable<unknown>, R>(nodes: [T1, T2, T3], selector: (values: [ObservableValue<T1>, ObservableValue<T2>, ObservableValue<T3>]) => R, equalityFn?: (a: R, b: R) => boolean): R;
|
|
159
|
+
export declare function useSelector<T1 extends Observable<unknown>, T2 extends Observable<unknown>, T3 extends Observable<unknown>, T4 extends Observable<unknown>, R>(nodes: [T1, T2, T3, T4], selector: (values: [ObservableValue<T1>, ObservableValue<T2>, ObservableValue<T3>, ObservableValue<T4>]) => R, equalityFn?: (a: R, b: R) => boolean): R;
|
|
160
|
+
export declare function useSelector<T1 extends Observable<unknown>, T2 extends Observable<unknown>, T3 extends Observable<unknown>, T4 extends Observable<unknown>, T5 extends Observable<unknown>, R>(nodes: [T1, T2, T3, T4, T5], selector: (values: [ObservableValue<T1>, ObservableValue<T2>, ObservableValue<T3>, ObservableValue<T4>, ObservableValue<T5>]) => R, equalityFn?: (a: R, b: R) => boolean): R;
|
|
161
|
+
export declare function useSelector<T extends Record<string, Observable<unknown>>, R>(nodes: T, selector: (values: ObservableObjectValues<T>) => R, equalityFn?: (a: R, b: R) => boolean): R;
|
|
134
162
|
export {};
|
|
135
163
|
//# sourceMappingURL=hooks.d.ts.map
|
package/dist/hooks.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAIvC;;;GAGG;AACH,KAAK,eAAe,CAAC,CAAC,IAAI,CAAC,SAAS,UAAU,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AASpE;;GAEG;AACH,KAAK,sBAAsB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC,IAAI;KAC1E,CAAC,IAAI,MAAM,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACtC,CAAC;AAuBF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAC7B,WAAW,EAAE,UAAU,CAAC,CAAC,CAAC,EAC1B,WAAW,EAAE,MAAM,CAAC,GACnB,CAAC,CAkBH;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,UAAU,CAAC,OAAO,CAAC,EACzD,IAAI,EAAE,CAAC,GACN,eAAe,CAAC,CAAC,CAAC,CAuBpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqEG;AAEH,wBAAgB,WAAW,CAAC,CAAC,SAAS,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC,EAC1D,IAAI,EAAE,CAAC,EACP,QAAQ,EAAE,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,EAC1C,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,GACnC,CAAC,CAAC;AAEL,wBAAgB,WAAW,CACzB,EAAE,SAAS,UAAU,CAAC,OAAO,CAAC,EAC9B,EAAE,SAAS,UAAU,CAAC,OAAO,CAAC,EAC9B,CAAC,EAED,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EACf,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,eAAe,CAAC,EAAE,CAAC,EAAE,eAAe,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,EACnE,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,GACnC,CAAC,CAAC;AACL,wBAAgB,WAAW,CACzB,EAAE,SAAS,UAAU,CAAC,OAAO,CAAC,EAC9B,EAAE,SAAS,UAAU,CAAC,OAAO,CAAC,EAC9B,EAAE,SAAS,UAAU,CAAC,OAAO,CAAC,EAC9B,CAAC,EAED,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EACnB,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,eAAe,CAAC,EAAE,CAAC,EAAE,eAAe,CAAC,EAAE,CAAC,EAAE,eAAe,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,EACxF,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,GACnC,CAAC,CAAC;AACL,wBAAgB,WAAW,CACzB,EAAE,SAAS,UAAU,CAAC,OAAO,CAAC,EAC9B,EAAE,SAAS,UAAU,CAAC,OAAO,CAAC,EAC9B,EAAE,SAAS,UAAU,CAAC,OAAO,CAAC,EAC9B,EAAE,SAAS,UAAU,CAAC,OAAO,CAAC,EAC9B,CAAC,EAED,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EACvB,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,eAAe,CAAC,EAAE,CAAC,EAAE,eAAe,CAAC,EAAE,CAAC,EAAE,eAAe,CAAC,EAAE,CAAC,EAAE,eAAe,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,EAC7G,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,GACnC,CAAC,CAAC;AACL,wBAAgB,WAAW,CACzB,EAAE,SAAS,UAAU,CAAC,OAAO,CAAC,EAC9B,EAAE,SAAS,UAAU,CAAC,OAAO,CAAC,EAC9B,EAAE,SAAS,UAAU,CAAC,OAAO,CAAC,EAC9B,EAAE,SAAS,UAAU,CAAC,OAAO,CAAC,EAC9B,EAAE,SAAS,UAAU,CAAC,OAAO,CAAC,EAC9B,CAAC,EAED,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAC3B,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,eAAe,CAAC,EAAE,CAAC,EAAE,eAAe,CAAC,EAAE,CAAC,EAAE,eAAe,CAAC,EAAE,CAAC,EAAE,eAAe,CAAC,EAAE,CAAC,EAAE,eAAe,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,EAClI,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,GACnC,CAAC,CAAC;AAEL,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,EAC1E,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC,CAAC,KAAK,CAAC,EAClD,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,GACnC,CAAC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// src/hooks.ts
|
|
2
2
|
import { useSyncExternalStore, useMemo, useCallback, useRef } from "react";
|
|
3
|
+
import { combineLatest } from "rxjs";
|
|
3
4
|
import { map, distinctUntilChanged } from "rxjs/operators";
|
|
4
5
|
function hasGet(obj) {
|
|
5
6
|
return obj !== null && typeof obj === "object" && "get" in obj && typeof obj.get === "function";
|
|
6
7
|
}
|
|
8
|
+
function isObservable(obj) {
|
|
9
|
+
return obj !== null && typeof obj === "object" && "subscribe" in obj && typeof obj.subscribe === "function";
|
|
10
|
+
}
|
|
7
11
|
function useObservable(observable$, getSnapshot) {
|
|
8
12
|
const valueRef = useRef(getSnapshot());
|
|
9
13
|
const subscribe = useCallback((onStoreChange) => {
|
|
@@ -28,22 +32,59 @@ function useStateValue(node) {
|
|
|
28
32
|
const getSnapshot = useCallback(() => valueRef.current, []);
|
|
29
33
|
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
30
34
|
}
|
|
31
|
-
function useSelector(
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return
|
|
35
|
+
function useSelector(nodeOrNodes, selector, equalityFn = Object.is) {
|
|
36
|
+
const { combined$, getInitialValue } = useMemo(() => {
|
|
37
|
+
if (Array.isArray(nodeOrNodes)) {
|
|
38
|
+
const nodes = nodeOrNodes;
|
|
39
|
+
return {
|
|
40
|
+
combined$: combineLatest(nodes).pipe(map((values) => selector(values)), distinctUntilChanged(equalityFn)),
|
|
41
|
+
getInitialValue: () => {
|
|
42
|
+
const values = nodes.map((n) => hasGet(n) ? n.get() : undefined);
|
|
43
|
+
return selector(values);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (!isObservable(nodeOrNodes)) {
|
|
48
|
+
const obj = nodeOrNodes;
|
|
49
|
+
const keys = Object.keys(obj);
|
|
50
|
+
const observables = keys.map((k) => obj[k]);
|
|
51
|
+
return {
|
|
52
|
+
combined$: combineLatest(observables).pipe(map((values) => {
|
|
53
|
+
const result = {};
|
|
54
|
+
keys.forEach((key, i) => {
|
|
55
|
+
result[key] = values[i];
|
|
56
|
+
});
|
|
57
|
+
return selector(result);
|
|
58
|
+
}), distinctUntilChanged(equalityFn)),
|
|
59
|
+
getInitialValue: () => {
|
|
60
|
+
const result = {};
|
|
61
|
+
keys.forEach((key) => {
|
|
62
|
+
const node2 = obj[key];
|
|
63
|
+
result[key] = hasGet(node2) ? node2.get() : undefined;
|
|
64
|
+
});
|
|
65
|
+
return selector(result);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
36
68
|
}
|
|
37
|
-
|
|
38
|
-
|
|
69
|
+
const node = nodeOrNodes;
|
|
70
|
+
return {
|
|
71
|
+
combined$: node.pipe(map((value) => selector(value)), distinctUntilChanged(equalityFn)),
|
|
72
|
+
getInitialValue: () => {
|
|
73
|
+
if (hasGet(node)) {
|
|
74
|
+
return selector(node.get());
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}, [nodeOrNodes, selector, equalityFn]);
|
|
39
80
|
const valueRef = useRef(getInitialValue());
|
|
40
81
|
const subscribe = useCallback((onStoreChange) => {
|
|
41
|
-
const subscription =
|
|
82
|
+
const subscription = combined$.subscribe((newValue) => {
|
|
42
83
|
valueRef.current = newValue;
|
|
43
84
|
onStoreChange();
|
|
44
85
|
});
|
|
45
86
|
return () => subscription.unsubscribe();
|
|
46
|
-
}, [
|
|
87
|
+
}, [combined$]);
|
|
47
88
|
const getSnapshot = useCallback(() => valueRef.current, []);
|
|
48
89
|
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
49
90
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@montra-interactive/deepstate-react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "React bindings for deepstate - Proxy-based reactive state management with RxJS.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"react": "^18 || ^19",
|
|
42
42
|
"rxjs": "^7",
|
|
43
|
-
"deepstate": "^0.1.0"
|
|
43
|
+
"@montra-interactive/deepstate": "^0.1.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@testing-library/react": "^16.3.1",
|
package/src/hooks.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useSyncExternalStore, useMemo, useCallback, useRef } from "react";
|
|
2
2
|
import type { Observable } from "rxjs";
|
|
3
|
+
import { combineLatest } from "rxjs";
|
|
3
4
|
import { map, distinctUntilChanged } from "rxjs/operators";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -8,6 +9,20 @@ import { map, distinctUntilChanged } from "rxjs/operators";
|
|
|
8
9
|
*/
|
|
9
10
|
type ObservableValue<T> = T extends Observable<infer V> ? V : never;
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Type for array of observables -> tuple of their values
|
|
14
|
+
*/
|
|
15
|
+
type ObservableValues<T extends readonly Observable<unknown>[]> = {
|
|
16
|
+
[K in keyof T]: ObservableValue<T[K]>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Type for object of observables -> object of their values
|
|
21
|
+
*/
|
|
22
|
+
type ObservableObjectValues<T extends Record<string, Observable<unknown>>> = {
|
|
23
|
+
[K in keyof T]: ObservableValue<T[K]>;
|
|
24
|
+
};
|
|
25
|
+
|
|
11
26
|
/**
|
|
12
27
|
* Interface for deepstate nodes that have a synchronous get() method.
|
|
13
28
|
* This is used internally to detect deepstate nodes vs plain observables.
|
|
@@ -20,6 +35,15 @@ function hasGet<T>(obj: unknown): obj is NodeWithGet<T> {
|
|
|
20
35
|
return obj !== null && typeof obj === "object" && "get" in obj && typeof (obj as NodeWithGet<T>).get === "function";
|
|
21
36
|
}
|
|
22
37
|
|
|
38
|
+
function isObservable(obj: unknown): obj is Observable<unknown> {
|
|
39
|
+
return (
|
|
40
|
+
obj !== null &&
|
|
41
|
+
typeof obj === "object" &&
|
|
42
|
+
"subscribe" in obj &&
|
|
43
|
+
typeof (obj as Record<string, unknown>).subscribe === "function"
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
23
47
|
/**
|
|
24
48
|
* Hook to subscribe to any Observable and get its current value.
|
|
25
49
|
* Re-renders the component whenever the observable emits a new value.
|
|
@@ -142,7 +166,7 @@ export function useStateValue<T extends Observable<unknown>>(
|
|
|
142
166
|
}
|
|
143
167
|
|
|
144
168
|
/**
|
|
145
|
-
* Hook to derive a value from
|
|
169
|
+
* Hook to derive a value from one or more deepstate nodes with a selector function.
|
|
146
170
|
* Only re-renders when the derived value changes (using reference equality by default).
|
|
147
171
|
*
|
|
148
172
|
* Use this when you need to compute/transform a value from state.
|
|
@@ -150,12 +174,12 @@ export function useStateValue<T extends Observable<unknown>>(
|
|
|
150
174
|
*
|
|
151
175
|
* Uses React 18's useSyncExternalStore for concurrent-mode safety.
|
|
152
176
|
*
|
|
153
|
-
* @param node - A deepstate node to select from
|
|
154
|
-
* @param selector - Function to derive a value from the node's value
|
|
177
|
+
* @param node - A deepstate node, array of nodes, or object of nodes to select from
|
|
178
|
+
* @param selector - Function to derive a value from the node's value(s)
|
|
155
179
|
* @param equalityFn - Optional custom equality function (default: Object.is)
|
|
156
180
|
* @returns The derived value
|
|
157
181
|
*
|
|
158
|
-
* @example
|
|
182
|
+
* @example Single node
|
|
159
183
|
* ```tsx
|
|
160
184
|
* import { state } from 'deepstate';
|
|
161
185
|
* import { useSelector } from 'deepstate-react';
|
|
@@ -165,7 +189,7 @@ export function useStateValue<T extends Observable<unknown>>(
|
|
|
165
189
|
* items: [{ id: 1, price: 10 }, { id: 2, price: 20 }]
|
|
166
190
|
* });
|
|
167
191
|
*
|
|
168
|
-
* // Derive a computed value
|
|
192
|
+
* // Derive a computed value from a single node
|
|
169
193
|
* function FullName() {
|
|
170
194
|
* const fullName = useSelector(
|
|
171
195
|
* store.user,
|
|
@@ -173,17 +197,34 @@ export function useStateValue<T extends Observable<unknown>>(
|
|
|
173
197
|
* );
|
|
174
198
|
* return <span>{fullName}</span>;
|
|
175
199
|
* }
|
|
200
|
+
* ```
|
|
176
201
|
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
202
|
+
* @example Multiple nodes (array form)
|
|
203
|
+
* ```tsx
|
|
204
|
+
* // Combine multiple nodes - receives values as tuple
|
|
205
|
+
* function Progress() {
|
|
206
|
+
* const percentage = useSelector(
|
|
207
|
+
* [store.completed, store.total],
|
|
208
|
+
* ([completed, total]) => total > 0 ? (completed / total) * 100 : 0
|
|
209
|
+
* );
|
|
210
|
+
* return <span>{percentage}%</span>;
|
|
211
|
+
* }
|
|
212
|
+
* ```
|
|
213
|
+
*
|
|
214
|
+
* @example Multiple nodes (object form)
|
|
215
|
+
* ```tsx
|
|
216
|
+
* // Combine multiple nodes - receives values as object
|
|
217
|
+
* function Progress() {
|
|
218
|
+
* const percentage = useSelector(
|
|
219
|
+
* { completed: store.completed, total: store.total },
|
|
220
|
+
* ({ completed, total }) => total > 0 ? (completed / total) * 100 : 0
|
|
182
221
|
* );
|
|
183
|
-
* return <span>
|
|
222
|
+
* return <span>{percentage}%</span>;
|
|
184
223
|
* }
|
|
224
|
+
* ```
|
|
185
225
|
*
|
|
186
|
-
*
|
|
226
|
+
* @example With custom equality
|
|
227
|
+
* ```tsx
|
|
187
228
|
* function ItemIds() {
|
|
188
229
|
* const ids = useSelector(
|
|
189
230
|
* store.items,
|
|
@@ -194,43 +235,144 @@ export function useStateValue<T extends Observable<unknown>>(
|
|
|
194
235
|
* }
|
|
195
236
|
* ```
|
|
196
237
|
*/
|
|
238
|
+
// Single node overload
|
|
197
239
|
export function useSelector<T extends Observable<unknown>, R>(
|
|
198
240
|
node: T,
|
|
199
241
|
selector: (value: ObservableValue<T>) => R,
|
|
200
|
-
equalityFn
|
|
201
|
-
): R
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
)
|
|
242
|
+
equalityFn?: (a: R, b: R) => boolean
|
|
243
|
+
): R;
|
|
244
|
+
// Array of nodes overload
|
|
245
|
+
export function useSelector<
|
|
246
|
+
T1 extends Observable<unknown>,
|
|
247
|
+
T2 extends Observable<unknown>,
|
|
248
|
+
R
|
|
249
|
+
>(
|
|
250
|
+
nodes: [T1, T2],
|
|
251
|
+
selector: (values: [ObservableValue<T1>, ObservableValue<T2>]) => R,
|
|
252
|
+
equalityFn?: (a: R, b: R) => boolean
|
|
253
|
+
): R;
|
|
254
|
+
export function useSelector<
|
|
255
|
+
T1 extends Observable<unknown>,
|
|
256
|
+
T2 extends Observable<unknown>,
|
|
257
|
+
T3 extends Observable<unknown>,
|
|
258
|
+
R
|
|
259
|
+
>(
|
|
260
|
+
nodes: [T1, T2, T3],
|
|
261
|
+
selector: (values: [ObservableValue<T1>, ObservableValue<T2>, ObservableValue<T3>]) => R,
|
|
262
|
+
equalityFn?: (a: R, b: R) => boolean
|
|
263
|
+
): R;
|
|
264
|
+
export function useSelector<
|
|
265
|
+
T1 extends Observable<unknown>,
|
|
266
|
+
T2 extends Observable<unknown>,
|
|
267
|
+
T3 extends Observable<unknown>,
|
|
268
|
+
T4 extends Observable<unknown>,
|
|
269
|
+
R
|
|
270
|
+
>(
|
|
271
|
+
nodes: [T1, T2, T3, T4],
|
|
272
|
+
selector: (values: [ObservableValue<T1>, ObservableValue<T2>, ObservableValue<T3>, ObservableValue<T4>]) => R,
|
|
273
|
+
equalityFn?: (a: R, b: R) => boolean
|
|
274
|
+
): R;
|
|
275
|
+
export function useSelector<
|
|
276
|
+
T1 extends Observable<unknown>,
|
|
277
|
+
T2 extends Observable<unknown>,
|
|
278
|
+
T3 extends Observable<unknown>,
|
|
279
|
+
T4 extends Observable<unknown>,
|
|
280
|
+
T5 extends Observable<unknown>,
|
|
281
|
+
R
|
|
282
|
+
>(
|
|
283
|
+
nodes: [T1, T2, T3, T4, T5],
|
|
284
|
+
selector: (values: [ObservableValue<T1>, ObservableValue<T2>, ObservableValue<T3>, ObservableValue<T4>, ObservableValue<T5>]) => R,
|
|
285
|
+
equalityFn?: (a: R, b: R) => boolean
|
|
286
|
+
): R;
|
|
287
|
+
// Object of nodes overload
|
|
288
|
+
export function useSelector<T extends Record<string, Observable<unknown>>, R>(
|
|
289
|
+
nodes: T,
|
|
290
|
+
selector: (values: ObservableObjectValues<T>) => R,
|
|
291
|
+
equalityFn?: (a: R, b: R) => boolean
|
|
292
|
+
): R;
|
|
293
|
+
// Implementation
|
|
294
|
+
export function useSelector(
|
|
295
|
+
nodeOrNodes: Observable<unknown> | Observable<unknown>[] | Record<string, Observable<unknown>>,
|
|
296
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
297
|
+
selector: (value: any) => any,
|
|
298
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
299
|
+
equalityFn: (a: any, b: any) => boolean = Object.is
|
|
300
|
+
): unknown {
|
|
301
|
+
// Determine the form and create the combined observable
|
|
302
|
+
const { combined$, getInitialValue } = useMemo(() => {
|
|
303
|
+
// Array form: [node1, node2, ...]
|
|
304
|
+
if (Array.isArray(nodeOrNodes)) {
|
|
305
|
+
const nodes = nodeOrNodes as Observable<unknown>[];
|
|
306
|
+
return {
|
|
307
|
+
combined$: combineLatest(nodes).pipe(
|
|
308
|
+
map((values) => selector(values)),
|
|
309
|
+
distinctUntilChanged(equalityFn)
|
|
310
|
+
),
|
|
311
|
+
getInitialValue: (): unknown => {
|
|
312
|
+
const values = nodes.map((n) => (hasGet<unknown>(n) ? n.get() : undefined));
|
|
313
|
+
return selector(values);
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Object form: { a: node1, b: node2, ... }
|
|
319
|
+
if (!isObservable(nodeOrNodes)) {
|
|
320
|
+
const obj = nodeOrNodes as Record<string, Observable<unknown>>;
|
|
321
|
+
const keys = Object.keys(obj);
|
|
322
|
+
const observables = keys.map((k) => obj[k]);
|
|
211
323
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
324
|
+
return {
|
|
325
|
+
combined$: combineLatest(observables).pipe(
|
|
326
|
+
map((values) => {
|
|
327
|
+
const result: Record<string, unknown> = {};
|
|
328
|
+
keys.forEach((key, i) => {
|
|
329
|
+
result[key] = values[i];
|
|
330
|
+
});
|
|
331
|
+
return selector(result);
|
|
332
|
+
}),
|
|
333
|
+
distinctUntilChanged(equalityFn)
|
|
334
|
+
),
|
|
335
|
+
getInitialValue: (): unknown => {
|
|
336
|
+
const result: Record<string, unknown> = {};
|
|
337
|
+
keys.forEach((key) => {
|
|
338
|
+
const node = obj[key];
|
|
339
|
+
result[key] = hasGet<unknown>(node) ? node.get() : undefined;
|
|
340
|
+
});
|
|
341
|
+
return selector(result);
|
|
342
|
+
},
|
|
343
|
+
};
|
|
216
344
|
}
|
|
217
|
-
|
|
218
|
-
|
|
345
|
+
|
|
346
|
+
// Single node form
|
|
347
|
+
const node = nodeOrNodes as Observable<unknown>;
|
|
348
|
+
return {
|
|
349
|
+
combined$: node.pipe(
|
|
350
|
+
map((value) => selector(value)),
|
|
351
|
+
distinctUntilChanged(equalityFn)
|
|
352
|
+
),
|
|
353
|
+
getInitialValue: (): unknown => {
|
|
354
|
+
if (hasGet<unknown>(node)) {
|
|
355
|
+
return selector(node.get());
|
|
356
|
+
}
|
|
357
|
+
return undefined;
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
}, [nodeOrNodes, selector, equalityFn]);
|
|
219
361
|
|
|
220
362
|
// Ref to hold the current derived value
|
|
221
|
-
const valueRef = useRef<
|
|
363
|
+
const valueRef = useRef<unknown>(getInitialValue());
|
|
222
364
|
|
|
223
365
|
// Subscribe callback for useSyncExternalStore
|
|
224
366
|
const subscribe = useCallback(
|
|
225
367
|
(onStoreChange: () => void) => {
|
|
226
|
-
const subscription =
|
|
368
|
+
const subscription = combined$.subscribe((newValue) => {
|
|
227
369
|
valueRef.current = newValue;
|
|
228
370
|
onStoreChange();
|
|
229
371
|
});
|
|
230
372
|
|
|
231
373
|
return () => subscription.unsubscribe();
|
|
232
374
|
},
|
|
233
|
-
[
|
|
375
|
+
[combined$]
|
|
234
376
|
);
|
|
235
377
|
|
|
236
378
|
// Get snapshot - just returns the ref value
|