@montra-interactive/deepstate-react 0.1.2 → 0.1.4

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