@montra-interactive/deepstate-react 0.1.4 → 0.2.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/dist/hooks.d.ts +27 -53
- package/dist/hooks.d.ts.map +1 -1
- package/dist/index.d.ts +15 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -25
- package/package.json +1 -1
- package/src/hooks.ts +78 -103
- package/src/index.ts +17 -4
package/dist/hooks.d.ts
CHANGED
|
@@ -35,81 +35,46 @@ type ObservableObjectValues<T extends Record<string, Observable<unknown>>> = {
|
|
|
35
35
|
*/
|
|
36
36
|
export declare function useObservable<T>(observable$: Observable<T>, getSnapshot: () => T): T;
|
|
37
37
|
/**
|
|
38
|
-
* Hook to get
|
|
39
|
-
* Re-renders the component whenever the
|
|
38
|
+
* Hook to get values from one or more deepstate nodes, optionally with a selector function.
|
|
39
|
+
* Re-renders the component whenever the selected value changes.
|
|
40
40
|
*
|
|
41
41
|
* This is the primary hook for using deepstate in React.
|
|
42
|
-
* Works with any deepstate node: RxLeaf, RxObject, RxArray, or RxNullable.
|
|
43
|
-
*
|
|
44
42
|
* Uses React 18's useSyncExternalStore for concurrent-mode safety.
|
|
45
43
|
*
|
|
46
|
-
* @
|
|
47
|
-
* @returns The current value of the node (deeply readonly)
|
|
48
|
-
*
|
|
49
|
-
* @example
|
|
44
|
+
* @example Single node (get raw value)
|
|
50
45
|
* ```tsx
|
|
51
46
|
* import { state } from 'deepstate';
|
|
52
|
-
* import {
|
|
47
|
+
* import { useSelect } from 'deepstate-react';
|
|
53
48
|
*
|
|
54
49
|
* const store = state({
|
|
55
50
|
* user: { name: 'Alice', age: 30 },
|
|
56
|
-
* items: [{ id: 1, name: 'Item 1' }],
|
|
57
51
|
* count: 0
|
|
58
52
|
* });
|
|
59
53
|
*
|
|
60
54
|
* // Subscribe to a primitive
|
|
61
55
|
* function Counter() {
|
|
62
|
-
* const count =
|
|
56
|
+
* const count = useSelect(store.count);
|
|
63
57
|
* return <span>{count}</span>;
|
|
64
58
|
* }
|
|
65
59
|
*
|
|
66
60
|
* // Subscribe to an object
|
|
67
61
|
* function UserCard() {
|
|
68
|
-
* const user =
|
|
62
|
+
* const user = useSelect(store.user);
|
|
69
63
|
* return <div>{user.name}, {user.age}</div>;
|
|
70
64
|
* }
|
|
71
65
|
*
|
|
72
66
|
* // Subscribe to a nested property (fine-grained!)
|
|
73
67
|
* function UserName() {
|
|
74
|
-
* const name =
|
|
68
|
+
* const name = useSelect(store.user.name);
|
|
75
69
|
* return <span>{name}</span>;
|
|
76
70
|
* }
|
|
77
|
-
*
|
|
78
|
-
* // Subscribe to an array
|
|
79
|
-
* function ItemList() {
|
|
80
|
-
* const items = useStateValue(store.items);
|
|
81
|
-
* return <ul>{items.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
|
|
82
|
-
* }
|
|
83
71
|
* ```
|
|
84
|
-
*/
|
|
85
|
-
export declare function useStateValue<T extends Observable<unknown>>(node: T): ObservableValue<T>;
|
|
86
|
-
/**
|
|
87
|
-
* Hook to derive a value from one or more deepstate nodes with a selector function.
|
|
88
|
-
* Only re-renders when the derived value changes (using reference equality by default).
|
|
89
|
-
*
|
|
90
|
-
* Use this when you need to compute/transform a value from state.
|
|
91
|
-
* The selector runs on every emission but only triggers re-render if result changes.
|
|
92
|
-
*
|
|
93
|
-
* Uses React 18's useSyncExternalStore for concurrent-mode safety.
|
|
94
72
|
*
|
|
95
|
-
* @
|
|
96
|
-
* @param selector - Function to derive a value from the node's value(s)
|
|
97
|
-
* @param equalityFn - Optional custom equality function (default: Object.is)
|
|
98
|
-
* @returns The derived value
|
|
99
|
-
*
|
|
100
|
-
* @example Single node
|
|
73
|
+
* @example Single node with selector (derive a value)
|
|
101
74
|
* ```tsx
|
|
102
|
-
* import { state } from 'deepstate';
|
|
103
|
-
* import { useSelector } from 'deepstate-react';
|
|
104
|
-
*
|
|
105
|
-
* const store = state({
|
|
106
|
-
* user: { firstName: 'Alice', lastName: 'Smith', age: 30 },
|
|
107
|
-
* items: [{ id: 1, price: 10 }, { id: 2, price: 20 }]
|
|
108
|
-
* });
|
|
109
|
-
*
|
|
110
75
|
* // Derive a computed value from a single node
|
|
111
76
|
* function FullName() {
|
|
112
|
-
* const fullName =
|
|
77
|
+
* const fullName = useSelect(
|
|
113
78
|
* store.user,
|
|
114
79
|
* user => `${user.firstName} ${user.lastName}`
|
|
115
80
|
* );
|
|
@@ -121,7 +86,7 @@ export declare function useStateValue<T extends Observable<unknown>>(node: T): O
|
|
|
121
86
|
* ```tsx
|
|
122
87
|
* // Combine multiple nodes - receives values as tuple
|
|
123
88
|
* function Progress() {
|
|
124
|
-
* const percentage =
|
|
89
|
+
* const percentage = useSelect(
|
|
125
90
|
* [store.completed, store.total],
|
|
126
91
|
* ([completed, total]) => total > 0 ? (completed / total) * 100 : 0
|
|
127
92
|
* );
|
|
@@ -133,7 +98,7 @@ export declare function useStateValue<T extends Observable<unknown>>(node: T): O
|
|
|
133
98
|
* ```tsx
|
|
134
99
|
* // Combine multiple nodes - receives values as object
|
|
135
100
|
* function Progress() {
|
|
136
|
-
* const percentage =
|
|
101
|
+
* const percentage = useSelect(
|
|
137
102
|
* { completed: store.completed, total: store.total },
|
|
138
103
|
* ({ completed, total }) => total > 0 ? (completed / total) * 100 : 0
|
|
139
104
|
* );
|
|
@@ -144,7 +109,7 @@ export declare function useStateValue<T extends Observable<unknown>>(node: T): O
|
|
|
144
109
|
* @example With custom equality
|
|
145
110
|
* ```tsx
|
|
146
111
|
* function ItemIds() {
|
|
147
|
-
* const ids =
|
|
112
|
+
* const ids = useSelect(
|
|
148
113
|
* store.items,
|
|
149
114
|
* items => items.map(i => i.id),
|
|
150
115
|
* (a, b) => a.length === b.length && a.every((v, i) => v === b[i])
|
|
@@ -153,11 +118,20 @@ export declare function useStateValue<T extends Observable<unknown>>(node: T): O
|
|
|
153
118
|
* }
|
|
154
119
|
* ```
|
|
155
120
|
*/
|
|
156
|
-
export declare function
|
|
157
|
-
export declare function
|
|
158
|
-
export declare function
|
|
159
|
-
export declare function
|
|
160
|
-
export declare function
|
|
161
|
-
export declare function
|
|
121
|
+
export declare function useSelect<T extends Observable<unknown>>(node: T): ObservableValue<T>;
|
|
122
|
+
export declare function useSelect<T extends Observable<unknown>, R>(node: T, selector: (value: ObservableValue<T>) => R, equalityFn?: (a: R, b: R) => boolean): R;
|
|
123
|
+
export declare function useSelect<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;
|
|
124
|
+
export declare function useSelect<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;
|
|
125
|
+
export declare function useSelect<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;
|
|
126
|
+
export declare function useSelect<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;
|
|
127
|
+
export declare function useSelect<T extends Record<string, Observable<unknown>>, R>(nodes: T, selector: (values: ObservableObjectValues<T>) => R, equalityFn?: (a: R, b: R) => boolean): R;
|
|
128
|
+
/**
|
|
129
|
+
* @deprecated Use `useSelect` instead. This is an alias for backwards compatibility.
|
|
130
|
+
*/
|
|
131
|
+
export declare const useStateValue: typeof useSelect;
|
|
132
|
+
/**
|
|
133
|
+
* @deprecated Use `useSelect` instead. This is an alias for backwards compatibility.
|
|
134
|
+
*/
|
|
135
|
+
export declare const useSelector: typeof useSelect;
|
|
162
136
|
export {};
|
|
163
137
|
//# 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;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
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,21 +6,32 @@
|
|
|
6
6
|
* @example
|
|
7
7
|
* ```tsx
|
|
8
8
|
* import { state } from 'deepstate';
|
|
9
|
-
* import {
|
|
9
|
+
* import { useSelect } from 'deepstate-react';
|
|
10
10
|
*
|
|
11
11
|
* const store = state({ user: { name: 'Alice', age: 30 }, count: 0 });
|
|
12
12
|
*
|
|
13
|
+
* // Get raw value
|
|
13
14
|
* function UserName() {
|
|
14
|
-
* const name =
|
|
15
|
+
* const name = useSelect(store.user.name);
|
|
15
16
|
* return <span>{name}</span>;
|
|
16
17
|
* }
|
|
17
18
|
*
|
|
19
|
+
* // With selector
|
|
18
20
|
* function UserSummary() {
|
|
19
|
-
* const summary =
|
|
21
|
+
* const summary = useSelect(store.user, user => `${user.name} (${user.age})`);
|
|
20
22
|
* return <span>{summary}</span>;
|
|
21
23
|
* }
|
|
24
|
+
*
|
|
25
|
+
* // Combine multiple nodes
|
|
26
|
+
* function Progress() {
|
|
27
|
+
* const pct = useSelect(
|
|
28
|
+
* [store.completed, store.total],
|
|
29
|
+
* ([completed, total]) => total > 0 ? (completed / total) * 100 : 0
|
|
30
|
+
* );
|
|
31
|
+
* return <span>{pct}%</span>;
|
|
32
|
+
* }
|
|
22
33
|
* ```
|
|
23
34
|
*/
|
|
24
|
-
export { useStateValue, useSelector,
|
|
35
|
+
export { useSelect, useObservable, useStateValue, useSelector, } from "./hooks";
|
|
25
36
|
export type { Observable } from "rxjs";
|
|
26
37
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EACL,SAAS,EACT,aAAa,EAEb,aAAa,EACb,WAAW,GACZ,MAAM,SAAS,CAAC;AAEjB,YAAY,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -20,27 +20,16 @@ function useObservable(observable$, getSnapshot) {
|
|
|
20
20
|
const getSnapshotMemo = useCallback(() => valueRef.current, []);
|
|
21
21
|
return useSyncExternalStore(subscribe, getSnapshotMemo, getSnapshotMemo);
|
|
22
22
|
}
|
|
23
|
-
function
|
|
24
|
-
const valueRef = useRef(hasGet(node) ? node.get() : undefined);
|
|
25
|
-
const subscribe = useCallback((onStoreChange) => {
|
|
26
|
-
const subscription = node.subscribe((newValue) => {
|
|
27
|
-
valueRef.current = newValue;
|
|
28
|
-
onStoreChange();
|
|
29
|
-
});
|
|
30
|
-
return () => subscription.unsubscribe();
|
|
31
|
-
}, [node]);
|
|
32
|
-
const getSnapshot = useCallback(() => valueRef.current, []);
|
|
33
|
-
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
34
|
-
}
|
|
35
|
-
function useSelector(nodeOrNodes, selector, equalityFn = Object.is) {
|
|
23
|
+
function useSelect(nodeOrNodes, selector, equalityFn = Object.is) {
|
|
36
24
|
const { combined$, getInitialValue } = useMemo(() => {
|
|
37
25
|
if (Array.isArray(nodeOrNodes)) {
|
|
38
26
|
const nodes = nodeOrNodes;
|
|
27
|
+
const sel = selector;
|
|
39
28
|
return {
|
|
40
|
-
combined$: combineLatest(nodes).pipe(map((values) =>
|
|
29
|
+
combined$: combineLatest(nodes).pipe(map((values) => sel(values)), distinctUntilChanged(equalityFn)),
|
|
41
30
|
getInitialValue: () => {
|
|
42
31
|
const values = nodes.map((n) => hasGet(n) ? n.get() : undefined);
|
|
43
|
-
return
|
|
32
|
+
return sel(values);
|
|
44
33
|
}
|
|
45
34
|
};
|
|
46
35
|
}
|
|
@@ -48,13 +37,14 @@ function useSelector(nodeOrNodes, selector, equalityFn = Object.is) {
|
|
|
48
37
|
const obj = nodeOrNodes;
|
|
49
38
|
const keys = Object.keys(obj);
|
|
50
39
|
const observables = keys.map((k) => obj[k]);
|
|
40
|
+
const sel = selector;
|
|
51
41
|
return {
|
|
52
42
|
combined$: combineLatest(observables).pipe(map((values) => {
|
|
53
43
|
const result = {};
|
|
54
44
|
keys.forEach((key, i) => {
|
|
55
45
|
result[key] = values[i];
|
|
56
46
|
});
|
|
57
|
-
return
|
|
47
|
+
return sel(result);
|
|
58
48
|
}), distinctUntilChanged(equalityFn)),
|
|
59
49
|
getInitialValue: () => {
|
|
60
50
|
const result = {};
|
|
@@ -62,20 +52,32 @@ function useSelector(nodeOrNodes, selector, equalityFn = Object.is) {
|
|
|
62
52
|
const node2 = obj[key];
|
|
63
53
|
result[key] = hasGet(node2) ? node2.get() : undefined;
|
|
64
54
|
});
|
|
65
|
-
return
|
|
55
|
+
return sel(result);
|
|
66
56
|
}
|
|
67
57
|
};
|
|
68
58
|
}
|
|
69
59
|
const node = nodeOrNodes;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
60
|
+
if (selector) {
|
|
61
|
+
return {
|
|
62
|
+
combined$: node.pipe(map((value) => selector(value)), distinctUntilChanged(equalityFn)),
|
|
63
|
+
getInitialValue: () => {
|
|
64
|
+
if (hasGet(node)) {
|
|
65
|
+
return selector(node.get());
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
} else {
|
|
71
|
+
return {
|
|
72
|
+
combined$: node.pipe(distinctUntilChanged(equalityFn)),
|
|
73
|
+
getInitialValue: () => {
|
|
74
|
+
if (hasGet(node)) {
|
|
75
|
+
return node.get();
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
75
78
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
};
|
|
79
|
+
};
|
|
80
|
+
}
|
|
79
81
|
}, [nodeOrNodes, selector, equalityFn]);
|
|
80
82
|
const valueRef = useRef(getInitialValue());
|
|
81
83
|
const subscribe = useCallback((onStoreChange) => {
|
|
@@ -88,8 +90,11 @@ function useSelector(nodeOrNodes, selector, equalityFn = Object.is) {
|
|
|
88
90
|
const getSnapshot = useCallback(() => valueRef.current, []);
|
|
89
91
|
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
90
92
|
}
|
|
93
|
+
var useStateValue = useSelect;
|
|
94
|
+
var useSelector = useSelect;
|
|
91
95
|
export {
|
|
92
96
|
useStateValue,
|
|
93
97
|
useSelector,
|
|
98
|
+
useSelect,
|
|
94
99
|
useObservable
|
|
95
100
|
};
|
package/package.json
CHANGED
package/src/hooks.ts
CHANGED
|
@@ -91,107 +91,46 @@ export function useObservable<T>(
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
|
-
* Hook to get
|
|
95
|
-
* Re-renders the component whenever the
|
|
94
|
+
* Hook to get values from one or more deepstate nodes, optionally with a selector function.
|
|
95
|
+
* Re-renders the component whenever the selected value changes.
|
|
96
96
|
*
|
|
97
97
|
* This is the primary hook for using deepstate in React.
|
|
98
|
-
* Works with any deepstate node: RxLeaf, RxObject, RxArray, or RxNullable.
|
|
99
|
-
*
|
|
100
98
|
* Uses React 18's useSyncExternalStore for concurrent-mode safety.
|
|
101
99
|
*
|
|
102
|
-
* @
|
|
103
|
-
* @returns The current value of the node (deeply readonly)
|
|
104
|
-
*
|
|
105
|
-
* @example
|
|
100
|
+
* @example Single node (get raw value)
|
|
106
101
|
* ```tsx
|
|
107
102
|
* import { state } from 'deepstate';
|
|
108
|
-
* import {
|
|
103
|
+
* import { useSelect } from 'deepstate-react';
|
|
109
104
|
*
|
|
110
105
|
* const store = state({
|
|
111
106
|
* user: { name: 'Alice', age: 30 },
|
|
112
|
-
* items: [{ id: 1, name: 'Item 1' }],
|
|
113
107
|
* count: 0
|
|
114
108
|
* });
|
|
115
109
|
*
|
|
116
110
|
* // Subscribe to a primitive
|
|
117
111
|
* function Counter() {
|
|
118
|
-
* const count =
|
|
112
|
+
* const count = useSelect(store.count);
|
|
119
113
|
* return <span>{count}</span>;
|
|
120
114
|
* }
|
|
121
115
|
*
|
|
122
116
|
* // Subscribe to an object
|
|
123
117
|
* function UserCard() {
|
|
124
|
-
* const user =
|
|
118
|
+
* const user = useSelect(store.user);
|
|
125
119
|
* return <div>{user.name}, {user.age}</div>;
|
|
126
120
|
* }
|
|
127
121
|
*
|
|
128
122
|
* // Subscribe to a nested property (fine-grained!)
|
|
129
123
|
* function UserName() {
|
|
130
|
-
* const name =
|
|
124
|
+
* const name = useSelect(store.user.name);
|
|
131
125
|
* return <span>{name}</span>;
|
|
132
126
|
* }
|
|
133
|
-
*
|
|
134
|
-
* // Subscribe to an array
|
|
135
|
-
* function ItemList() {
|
|
136
|
-
* const items = useStateValue(store.items);
|
|
137
|
-
* return <ul>{items.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
|
|
138
|
-
* }
|
|
139
127
|
* ```
|
|
140
|
-
*/
|
|
141
|
-
export function useStateValue<T extends Observable<unknown>>(
|
|
142
|
-
node: T
|
|
143
|
-
): ObservableValue<T> {
|
|
144
|
-
// Ref to hold the current value - updated by subscription
|
|
145
|
-
const valueRef = useRef<ObservableValue<T>>(
|
|
146
|
-
hasGet<ObservableValue<T>>(node) ? node.get() : (undefined as ObservableValue<T>)
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
// Subscribe callback for useSyncExternalStore
|
|
150
|
-
const subscribe = useCallback(
|
|
151
|
-
(onStoreChange: () => void) => {
|
|
152
|
-
const subscription = node.subscribe((newValue) => {
|
|
153
|
-
valueRef.current = newValue as ObservableValue<T>;
|
|
154
|
-
onStoreChange();
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
return () => subscription.unsubscribe();
|
|
158
|
-
},
|
|
159
|
-
[node]
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
// Get snapshot - just returns the ref value
|
|
163
|
-
const getSnapshot = useCallback(() => valueRef.current, []);
|
|
164
|
-
|
|
165
|
-
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Hook to derive a value from one or more deepstate nodes with a selector function.
|
|
170
|
-
* Only re-renders when the derived value changes (using reference equality by default).
|
|
171
|
-
*
|
|
172
|
-
* Use this when you need to compute/transform a value from state.
|
|
173
|
-
* The selector runs on every emission but only triggers re-render if result changes.
|
|
174
128
|
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
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)
|
|
179
|
-
* @param equalityFn - Optional custom equality function (default: Object.is)
|
|
180
|
-
* @returns The derived value
|
|
181
|
-
*
|
|
182
|
-
* @example Single node
|
|
129
|
+
* @example Single node with selector (derive a value)
|
|
183
130
|
* ```tsx
|
|
184
|
-
* import { state } from 'deepstate';
|
|
185
|
-
* import { useSelector } from 'deepstate-react';
|
|
186
|
-
*
|
|
187
|
-
* const store = state({
|
|
188
|
-
* user: { firstName: 'Alice', lastName: 'Smith', age: 30 },
|
|
189
|
-
* items: [{ id: 1, price: 10 }, { id: 2, price: 20 }]
|
|
190
|
-
* });
|
|
191
|
-
*
|
|
192
131
|
* // Derive a computed value from a single node
|
|
193
132
|
* function FullName() {
|
|
194
|
-
* const fullName =
|
|
133
|
+
* const fullName = useSelect(
|
|
195
134
|
* store.user,
|
|
196
135
|
* user => `${user.firstName} ${user.lastName}`
|
|
197
136
|
* );
|
|
@@ -203,7 +142,7 @@ export function useStateValue<T extends Observable<unknown>>(
|
|
|
203
142
|
* ```tsx
|
|
204
143
|
* // Combine multiple nodes - receives values as tuple
|
|
205
144
|
* function Progress() {
|
|
206
|
-
* const percentage =
|
|
145
|
+
* const percentage = useSelect(
|
|
207
146
|
* [store.completed, store.total],
|
|
208
147
|
* ([completed, total]) => total > 0 ? (completed / total) * 100 : 0
|
|
209
148
|
* );
|
|
@@ -215,7 +154,7 @@ export function useStateValue<T extends Observable<unknown>>(
|
|
|
215
154
|
* ```tsx
|
|
216
155
|
* // Combine multiple nodes - receives values as object
|
|
217
156
|
* function Progress() {
|
|
218
|
-
* const percentage =
|
|
157
|
+
* const percentage = useSelect(
|
|
219
158
|
* { completed: store.completed, total: store.total },
|
|
220
159
|
* ({ completed, total }) => total > 0 ? (completed / total) * 100 : 0
|
|
221
160
|
* );
|
|
@@ -226,7 +165,7 @@ export function useStateValue<T extends Observable<unknown>>(
|
|
|
226
165
|
* @example With custom equality
|
|
227
166
|
* ```tsx
|
|
228
167
|
* function ItemIds() {
|
|
229
|
-
* const ids =
|
|
168
|
+
* const ids = useSelect(
|
|
230
169
|
* store.items,
|
|
231
170
|
* items => items.map(i => i.id),
|
|
232
171
|
* (a, b) => a.length === b.length && a.every((v, i) => v === b[i])
|
|
@@ -235,14 +174,18 @@ export function useStateValue<T extends Observable<unknown>>(
|
|
|
235
174
|
* }
|
|
236
175
|
* ```
|
|
237
176
|
*/
|
|
238
|
-
// Single node
|
|
239
|
-
export function
|
|
177
|
+
// Single node, no selector - return raw value
|
|
178
|
+
export function useSelect<T extends Observable<unknown>>(
|
|
179
|
+
node: T
|
|
180
|
+
): ObservableValue<T>;
|
|
181
|
+
// Single node with selector
|
|
182
|
+
export function useSelect<T extends Observable<unknown>, R>(
|
|
240
183
|
node: T,
|
|
241
184
|
selector: (value: ObservableValue<T>) => R,
|
|
242
185
|
equalityFn?: (a: R, b: R) => boolean
|
|
243
186
|
): R;
|
|
244
|
-
// Array of nodes
|
|
245
|
-
export function
|
|
187
|
+
// Array of 2 nodes with selector
|
|
188
|
+
export function useSelect<
|
|
246
189
|
T1 extends Observable<unknown>,
|
|
247
190
|
T2 extends Observable<unknown>,
|
|
248
191
|
R
|
|
@@ -251,7 +194,8 @@ export function useSelector<
|
|
|
251
194
|
selector: (values: [ObservableValue<T1>, ObservableValue<T2>]) => R,
|
|
252
195
|
equalityFn?: (a: R, b: R) => boolean
|
|
253
196
|
): R;
|
|
254
|
-
|
|
197
|
+
// Array of 3 nodes with selector
|
|
198
|
+
export function useSelect<
|
|
255
199
|
T1 extends Observable<unknown>,
|
|
256
200
|
T2 extends Observable<unknown>,
|
|
257
201
|
T3 extends Observable<unknown>,
|
|
@@ -261,7 +205,8 @@ export function useSelector<
|
|
|
261
205
|
selector: (values: [ObservableValue<T1>, ObservableValue<T2>, ObservableValue<T3>]) => R,
|
|
262
206
|
equalityFn?: (a: R, b: R) => boolean
|
|
263
207
|
): R;
|
|
264
|
-
|
|
208
|
+
// Array of 4 nodes with selector
|
|
209
|
+
export function useSelect<
|
|
265
210
|
T1 extends Observable<unknown>,
|
|
266
211
|
T2 extends Observable<unknown>,
|
|
267
212
|
T3 extends Observable<unknown>,
|
|
@@ -272,7 +217,8 @@ export function useSelector<
|
|
|
272
217
|
selector: (values: [ObservableValue<T1>, ObservableValue<T2>, ObservableValue<T3>, ObservableValue<T4>]) => R,
|
|
273
218
|
equalityFn?: (a: R, b: R) => boolean
|
|
274
219
|
): R;
|
|
275
|
-
|
|
220
|
+
// Array of 5 nodes with selector
|
|
221
|
+
export function useSelect<
|
|
276
222
|
T1 extends Observable<unknown>,
|
|
277
223
|
T2 extends Observable<unknown>,
|
|
278
224
|
T3 extends Observable<unknown>,
|
|
@@ -284,42 +230,44 @@ export function useSelector<
|
|
|
284
230
|
selector: (values: [ObservableValue<T1>, ObservableValue<T2>, ObservableValue<T3>, ObservableValue<T4>, ObservableValue<T5>]) => R,
|
|
285
231
|
equalityFn?: (a: R, b: R) => boolean
|
|
286
232
|
): R;
|
|
287
|
-
// Object of nodes
|
|
288
|
-
export function
|
|
233
|
+
// Object of nodes with selector
|
|
234
|
+
export function useSelect<T extends Record<string, Observable<unknown>>, R>(
|
|
289
235
|
nodes: T,
|
|
290
236
|
selector: (values: ObservableObjectValues<T>) => R,
|
|
291
237
|
equalityFn?: (a: R, b: R) => boolean
|
|
292
238
|
): R;
|
|
293
239
|
// Implementation
|
|
294
|
-
export function
|
|
240
|
+
export function useSelect(
|
|
295
241
|
nodeOrNodes: Observable<unknown> | Observable<unknown>[] | Record<string, Observable<unknown>>,
|
|
296
242
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
297
|
-
selector
|
|
243
|
+
selector?: (value: any) => any,
|
|
298
244
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
299
245
|
equalityFn: (a: any, b: any) => boolean = Object.is
|
|
300
246
|
): unknown {
|
|
301
247
|
// Determine the form and create the combined observable
|
|
302
248
|
const { combined$, getInitialValue } = useMemo(() => {
|
|
303
|
-
// Array form: [node1, node2, ...]
|
|
249
|
+
// Array form: [node1, node2, ...] - always requires selector
|
|
304
250
|
if (Array.isArray(nodeOrNodes)) {
|
|
305
251
|
const nodes = nodeOrNodes as Observable<unknown>[];
|
|
252
|
+
const sel = selector!; // selector is required for array form
|
|
306
253
|
return {
|
|
307
254
|
combined$: combineLatest(nodes).pipe(
|
|
308
|
-
map((values) =>
|
|
255
|
+
map((values) => sel(values)),
|
|
309
256
|
distinctUntilChanged(equalityFn)
|
|
310
257
|
),
|
|
311
258
|
getInitialValue: (): unknown => {
|
|
312
259
|
const values = nodes.map((n) => (hasGet<unknown>(n) ? n.get() : undefined));
|
|
313
|
-
return
|
|
260
|
+
return sel(values);
|
|
314
261
|
},
|
|
315
262
|
};
|
|
316
263
|
}
|
|
317
264
|
|
|
318
|
-
// Object form: { a: node1, b: node2, ... }
|
|
265
|
+
// Object form: { a: node1, b: node2, ... } - always requires selector
|
|
319
266
|
if (!isObservable(nodeOrNodes)) {
|
|
320
267
|
const obj = nodeOrNodes as Record<string, Observable<unknown>>;
|
|
321
268
|
const keys = Object.keys(obj);
|
|
322
269
|
const observables = keys.map((k) => obj[k]);
|
|
270
|
+
const sel = selector!; // selector is required for object form
|
|
323
271
|
|
|
324
272
|
return {
|
|
325
273
|
combined$: combineLatest(observables).pipe(
|
|
@@ -328,7 +276,7 @@ export function useSelector(
|
|
|
328
276
|
keys.forEach((key, i) => {
|
|
329
277
|
result[key] = values[i];
|
|
330
278
|
});
|
|
331
|
-
return
|
|
279
|
+
return sel(result);
|
|
332
280
|
}),
|
|
333
281
|
distinctUntilChanged(equalityFn)
|
|
334
282
|
),
|
|
@@ -338,25 +286,42 @@ export function useSelector(
|
|
|
338
286
|
const node = obj[key];
|
|
339
287
|
result[key] = hasGet<unknown>(node) ? node.get() : undefined;
|
|
340
288
|
});
|
|
341
|
-
return
|
|
289
|
+
return sel(result);
|
|
342
290
|
},
|
|
343
291
|
};
|
|
344
292
|
}
|
|
345
293
|
|
|
346
|
-
// Single node form
|
|
294
|
+
// Single node form - selector is optional
|
|
347
295
|
const node = nodeOrNodes as Observable<unknown>;
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
296
|
+
|
|
297
|
+
if (selector) {
|
|
298
|
+
// With selector - apply transformation
|
|
299
|
+
return {
|
|
300
|
+
combined$: node.pipe(
|
|
301
|
+
map((value) => selector(value)),
|
|
302
|
+
distinctUntilChanged(equalityFn)
|
|
303
|
+
),
|
|
304
|
+
getInitialValue: (): unknown => {
|
|
305
|
+
if (hasGet<unknown>(node)) {
|
|
306
|
+
return selector(node.get());
|
|
307
|
+
}
|
|
308
|
+
return undefined;
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
} else {
|
|
312
|
+
// No selector - return raw value
|
|
313
|
+
return {
|
|
314
|
+
combined$: node.pipe(
|
|
315
|
+
distinctUntilChanged(equalityFn)
|
|
316
|
+
),
|
|
317
|
+
getInitialValue: (): unknown => {
|
|
318
|
+
if (hasGet<unknown>(node)) {
|
|
319
|
+
return node.get();
|
|
320
|
+
}
|
|
321
|
+
return undefined;
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
360
325
|
}, [nodeOrNodes, selector, equalityFn]);
|
|
361
326
|
|
|
362
327
|
// Ref to hold the current derived value
|
|
@@ -380,3 +345,13 @@ export function useSelector(
|
|
|
380
345
|
|
|
381
346
|
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
382
347
|
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* @deprecated Use `useSelect` instead. This is an alias for backwards compatibility.
|
|
351
|
+
*/
|
|
352
|
+
export const useStateValue = useSelect;
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* @deprecated Use `useSelect` instead. This is an alias for backwards compatibility.
|
|
356
|
+
*/
|
|
357
|
+
export const useSelector = useSelect;
|
package/src/index.ts
CHANGED
|
@@ -6,26 +6,39 @@
|
|
|
6
6
|
* @example
|
|
7
7
|
* ```tsx
|
|
8
8
|
* import { state } from 'deepstate';
|
|
9
|
-
* import {
|
|
9
|
+
* import { useSelect } from 'deepstate-react';
|
|
10
10
|
*
|
|
11
11
|
* const store = state({ user: { name: 'Alice', age: 30 }, count: 0 });
|
|
12
12
|
*
|
|
13
|
+
* // Get raw value
|
|
13
14
|
* function UserName() {
|
|
14
|
-
* const name =
|
|
15
|
+
* const name = useSelect(store.user.name);
|
|
15
16
|
* return <span>{name}</span>;
|
|
16
17
|
* }
|
|
17
18
|
*
|
|
19
|
+
* // With selector
|
|
18
20
|
* function UserSummary() {
|
|
19
|
-
* const summary =
|
|
21
|
+
* const summary = useSelect(store.user, user => `${user.name} (${user.age})`);
|
|
20
22
|
* return <span>{summary}</span>;
|
|
21
23
|
* }
|
|
24
|
+
*
|
|
25
|
+
* // Combine multiple nodes
|
|
26
|
+
* function Progress() {
|
|
27
|
+
* const pct = useSelect(
|
|
28
|
+
* [store.completed, store.total],
|
|
29
|
+
* ([completed, total]) => total > 0 ? (completed / total) * 100 : 0
|
|
30
|
+
* );
|
|
31
|
+
* return <span>{pct}%</span>;
|
|
32
|
+
* }
|
|
22
33
|
* ```
|
|
23
34
|
*/
|
|
24
35
|
|
|
25
36
|
export {
|
|
37
|
+
useSelect,
|
|
38
|
+
useObservable,
|
|
39
|
+
// Deprecated aliases for backwards compatibility
|
|
26
40
|
useStateValue,
|
|
27
41
|
useSelector,
|
|
28
|
-
useObservable,
|
|
29
42
|
} from "./hooks";
|
|
30
43
|
|
|
31
44
|
export type { Observable } from "rxjs";
|