@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 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 the current value of a deepstate node.
33
- * Re-renders the component whenever the node's value changes.
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
- * @param node - A deepstate node (any reactive property from your state)
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 { useStateValue } from 'deepstate-react';
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 = useStateValue(store.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 = useStateValue(store.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 = useStateValue(store.user.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
- * Use this when you need to compute/transform a value from state.
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
- * import { state } from 'deepstate';
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 = useSelector(
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
- * // Derive from an array
114
- * function TotalPrice() {
115
- * const total = useSelector(
116
- * store.items,
117
- * items => items.reduce((sum, item) => sum + item.price, 0)
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>Total: ${total}</span>;
105
+ * return <span>{percentage}%</span>;
120
106
  * }
107
+ * ```
121
108
  *
122
- * // With custom equality (e.g., for arrays)
109
+ * @example With custom equality
110
+ * ```tsx
123
111
  * function ItemIds() {
124
- * const ids = useSelector(
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 useSelector<T extends Observable<unknown>, R>(node: T, selector: (value: ObservableValue<T>) => R, equalityFn?: (a: R, b: R) => boolean): R;
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
@@ -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;AAGvC;;;GAGG;AACH,KAAK,eAAe,CAAC,CAAC,IAAI,CAAC,SAAS,UAAU,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAcpE;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAC7B,WAAW,EAAE,UAAU,CAAC,CAAC,CAAC,EAC1B,WAAW,EAAE,MAAM,CAAC,GACnB,CAAC,CAkBH;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,UAAU,CAAC,OAAO,CAAC,EACzD,IAAI,EAAE,CAAC,GACN,eAAe,CAAC,CAAC,CAAC,CAuBpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC,EAC1D,IAAI,EAAE,CAAC,EACP,QAAQ,EAAE,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,EAC1C,UAAU,GAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAmB,GAC9C,CAAC,CAuCH"}
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 { useStateValue, useSelector } from 'deepstate-react';
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 = useStateValue(store.user.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 = useSelector(store.user, user => `${user.name} (${user.age})`);
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, useObservable, } from "./hooks";
35
+ export { useSelect, useObservable, useStateValue, useSelector, } from "./hooks";
25
36
  export type { Observable } from "rxjs";
26
37
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EACL,aAAa,EACb,WAAW,EACX,aAAa,GACd,MAAM,SAAS,CAAC;AAEjB,YAAY,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC"}
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 useStateValue(node) {
20
- const valueRef = useRef(hasGet(node) ? node.get() : undefined);
21
- const subscribe = useCallback((onStoreChange) => {
22
- const subscription = node.subscribe((newValue) => {
23
- valueRef.current = newValue;
24
- onStoreChange();
25
- });
26
- return () => subscription.unsubscribe();
27
- }, [node]);
28
- const getSnapshot = useCallback(() => valueRef.current, []);
29
- return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
30
- }
31
- function useSelector(node, selector, equalityFn = Object.is) {
32
- const derived$ = useMemo(() => node.pipe(map((value) => selector(value)), distinctUntilChanged(equalityFn)), [node, selector, equalityFn]);
33
- const getInitialValue = () => {
34
- if (hasGet(node)) {
35
- return selector(node.get());
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
- return;
38
- };
81
+ }, [nodeOrNodes, selector, equalityFn]);
39
82
  const valueRef = useRef(getInitialValue());
40
83
  const subscribe = useCallback((onStoreChange) => {
41
- const subscription = derived$.subscribe((newValue) => {
84
+ const subscription = combined$.subscribe((newValue) => {
42
85
  valueRef.current = newValue;
43
86
  onStoreChange();
44
87
  });
45
88
  return () => subscription.unsubscribe();
46
- }, [derived$]);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@montra-interactive/deepstate-react",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "React bindings for deepstate - Proxy-based reactive state management with RxJS.",
5
5
  "keywords": [
6
6
  "react",
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 the current value of a deepstate node.
71
- * Re-renders the component whenever the node's value changes.
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
- * @param node - A deepstate node (any reactive property from your state)
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 { useStateValue } from 'deepstate-react';
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 = useStateValue(store.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 = useStateValue(store.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 = useStateValue(store.user.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
- * import { state } from 'deepstate';
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 = useSelector(
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
- * // Derive from an array
178
- * function TotalPrice() {
179
- * const total = useSelector(
180
- * store.items,
181
- * items => items.reduce((sum, item) => sum + item.price, 0)
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>Total: ${total}</span>;
149
+ * return <span>{percentage}%</span>;
184
150
  * }
151
+ * ```
185
152
  *
186
- * // With custom equality (e.g., for arrays)
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 = useSelector(
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
- export function useSelector<T extends Observable<unknown>, R>(
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: (a: R, b: R) => boolean = Object.is
201
- ): R {
202
- // Create derived observable that applies selector and dedupes
203
- const derived$ = useMemo(
204
- () =>
205
- node.pipe(
206
- map((value) => selector(value as ObservableValue<T>)),
207
- distinctUntilChanged(equalityFn)
208
- ),
209
- [node, selector, equalityFn]
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
- // Get initial derived value
213
- const getInitialValue = (): R => {
214
- if (hasGet<ObservableValue<T>>(node)) {
215
- return selector(node.get());
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
- return undefined as R;
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<R>(getInitialValue());
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 = derived$.subscribe((newValue) => {
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
- [derived$]
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 { useStateValue, useSelector } from 'deepstate-react';
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 = useStateValue(store.user.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 = useSelector(store.user, user => `${user.name} (${user.age})`);
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";