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