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