@montra-interactive/deepstate-react 0.2.3 → 0.2.5

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/README.md ADDED
@@ -0,0 +1,374 @@
1
+ # @montra-interactive/deepstate-react
2
+
3
+ React bindings for [deepstate](https://www.npmjs.com/package/@montra-interactive/deepstate) - proxy-based reactive state management with RxJS.
4
+
5
+ ## Features
6
+
7
+ - **Fine-grained subscriptions**: Subscribe to any nested property
8
+ - **Concurrent mode safe**: Uses `useSyncExternalStore` for React 18+
9
+ - **Type-safe**: Full TypeScript support with inferred types
10
+ - **RxJS integration**: Use `usePipeSelect` for debouncing, filtering, mapping
11
+ - **Multiple node combining**: Array form (tuple) or object form (named keys)
12
+ - **Custom equality**: Prevent unnecessary re-renders with custom comparators
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @montra-interactive/deepstate @montra-interactive/deepstate-react rxjs
18
+ # or
19
+ bun add @montra-interactive/deepstate @montra-interactive/deepstate-react rxjs
20
+ # or
21
+ yarn add @montra-interactive/deepstate @montra-interactive/deepstate-react rxjs
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ```tsx
27
+ import { state } from "@montra-interactive/deepstate";
28
+ import { useSelect } from "@montra-interactive/deepstate-react";
29
+
30
+ // Create your store
31
+ const store = state({
32
+ user: { name: "Alice", age: 30 },
33
+ count: 0,
34
+ });
35
+
36
+ // Use in components
37
+ function UserName() {
38
+ const name = useSelect(store.user.name);
39
+ return <span>{name}</span>;
40
+ }
41
+
42
+ function Counter() {
43
+ const count = useSelect(store.count);
44
+ return (
45
+ <button onClick={() => store.count.set(count + 1)}>
46
+ Count: {count}
47
+ </button>
48
+ );
49
+ }
50
+ ```
51
+
52
+ ## API Reference
53
+
54
+ ### `useSelect` - Subscribe to Deepstate Nodes
55
+
56
+ The primary hook for using deepstate in React. Returns the current value and re-renders when it changes.
57
+
58
+ #### Single Node
59
+
60
+ ```tsx
61
+ const value = useSelect(store.user.name); // string
62
+ const user = useSelect(store.user); // { name: string, age: number }
63
+ ```
64
+
65
+ #### With Selector
66
+
67
+ Transform the value before returning. Only re-renders when the derived value changes.
68
+
69
+ ```tsx
70
+ const fullName = useSelect(
71
+ store.user,
72
+ user => `${user.firstName} ${user.lastName}`
73
+ );
74
+
75
+ const adultCount = useSelect(
76
+ store.users,
77
+ users => users.filter(u => u.age >= 18).length
78
+ );
79
+ ```
80
+
81
+ #### Multiple Nodes (Array Form)
82
+
83
+ Combine multiple nodes into a single derived value:
84
+
85
+ ```tsx
86
+ const percentage = useSelect(
87
+ [store.stats.completed, store.stats.total],
88
+ ([completed, total]) => total > 0 ? (completed / total) * 100 : 0
89
+ );
90
+ ```
91
+
92
+ #### Multiple Nodes (Object Form)
93
+
94
+ Same as array form, but with named keys:
95
+
96
+ ```tsx
97
+ const summary = useSelect(
98
+ {
99
+ name: store.user.name,
100
+ completed: store.stats.completed
101
+ },
102
+ ({ name, completed }) => `${name} completed ${completed} tasks`
103
+ );
104
+ ```
105
+
106
+ #### Custom Equality Function
107
+
108
+ Prevent re-renders with a custom equality check:
109
+
110
+ ```tsx
111
+ const ids = useSelect(
112
+ store.items,
113
+ items => items.map(i => i.id),
114
+ // Custom array equality
115
+ (a, b) => a.length === b.length && a.every((v, i) => v === b[i])
116
+ );
117
+ ```
118
+
119
+ ### `usePipeSelect` - Subscribe to Piped Observables
120
+
121
+ For observables transformed with RxJS operators. Returns `T | undefined` because the stream might not have emitted yet.
122
+
123
+ #### Debouncing
124
+
125
+ Reduce re-renders from high-frequency updates:
126
+
127
+ ```tsx
128
+ import { debounceTime } from "rxjs";
129
+
130
+ function DebouncedSearch() {
131
+ const query = usePipeSelect(
132
+ store.searchQuery.pipe(debounceTime(300))
133
+ );
134
+
135
+ if (query === undefined) {
136
+ return <span>Type to search...</span>;
137
+ }
138
+
139
+ return <SearchResults query={query} />;
140
+ }
141
+ ```
142
+
143
+ #### Filtering
144
+
145
+ Only emit when conditions are met:
146
+
147
+ ```tsx
148
+ import { filter } from "rxjs";
149
+
150
+ function PositiveOnly() {
151
+ const value = usePipeSelect(
152
+ store.count.pipe(filter(v => v > 0))
153
+ );
154
+
155
+ // undefined until count > 0
156
+ return <span>{value ?? "Waiting for positive..."}</span>;
157
+ }
158
+ ```
159
+
160
+ #### Mapping / Transforming
161
+
162
+ Transform values in the stream:
163
+
164
+ ```tsx
165
+ import { map } from "rxjs";
166
+
167
+ function TotalDuration() {
168
+ const total = usePipeSelect(
169
+ store.clips.pipe(
170
+ map(clips => clips.reduce((sum, c) => sum + c.duration, 0))
171
+ )
172
+ );
173
+
174
+ return <span>Total: {total ?? 0}ms</span>;
175
+ }
176
+ ```
177
+
178
+ #### Combined Operators
179
+
180
+ Chain multiple operators:
181
+
182
+ ```tsx
183
+ import { debounceTime, filter, map } from "rxjs";
184
+
185
+ function SmartSearch() {
186
+ const query = usePipeSelect(
187
+ store.searchQuery.pipe(
188
+ debounceTime(300),
189
+ filter(q => q.length >= 2),
190
+ map(q => q.trim().toLowerCase())
191
+ )
192
+ );
193
+
194
+ if (query === undefined) {
195
+ return <span>Type at least 2 characters...</span>;
196
+ }
197
+
198
+ return <SearchResults query={query} />;
199
+ }
200
+ ```
201
+
202
+ ### `useObservable` - Low-level Observable Hook
203
+
204
+ For any RxJS Observable when you need to provide the initial value getter:
205
+
206
+ ```tsx
207
+ import { BehaviorSubject } from "rxjs";
208
+
209
+ const count$ = new BehaviorSubject(0);
210
+
211
+ function Counter() {
212
+ const count = useObservable(count$, () => count$.getValue());
213
+ return <span>{count}</span>;
214
+ }
215
+ ```
216
+
217
+ ## Why Two Hooks?
218
+
219
+ ### The Sync/Async Boundary
220
+
221
+ deepstate is a **synchronous store** backed by **reactive streams**:
222
+
223
+ - `useSelect(store.x)` - Node has `.get()`, initial value always available. Returns `T`.
224
+ - `usePipeSelect(store.x.pipe(...))` - Piped stream has no sync value. Returns `T | undefined`.
225
+
226
+ When you `.pipe()` a node, you enter the async world of RxJS where:
227
+
228
+ | Operator | Why No Sync Value? |
229
+ |----------|-------------------|
230
+ | `debounceTime(300)` | Waits 300ms before emitting |
231
+ | `filter(v => v > 0)` | If value is `0`, nothing passed yet |
232
+ | `switchMap(...)` | Depends on async operation |
233
+
234
+ The `T | undefined` return type is **honest** - it forces you to handle the "not yet" case:
235
+
236
+ ```tsx
237
+ // useSelect - always has value
238
+ const count = useSelect(store.count);
239
+ const doubled = count * 2; // Safe
240
+
241
+ // usePipeSelect - might be undefined
242
+ const filtered = usePipeSelect(store.count.pipe(filter(v => v > 0)));
243
+ const doubled = (filtered ?? 0) * 2; // Must handle undefined
244
+ ```
245
+
246
+ ## Type Exports
247
+
248
+ ```ts
249
+ import type { DeepstateNode } from "@montra-interactive/deepstate-react";
250
+ ```
251
+
252
+ | Type | Description |
253
+ |------|-------------|
254
+ | `DeepstateNode<T>` | Observable with `.get()` - what `useSelect` accepts |
255
+
256
+ ## Full Type Signatures
257
+
258
+ ```ts
259
+ // useSelect overloads
260
+ function useSelect<T>(node: DeepstateNode<T>): T;
261
+
262
+ function useSelect<T, R>(
263
+ node: DeepstateNode<T>,
264
+ selector: (value: T) => R,
265
+ equalityFn?: (a: R, b: R) => boolean
266
+ ): R;
267
+
268
+ function useSelect<T1, T2, R>(
269
+ nodes: [DeepstateNode<T1>, DeepstateNode<T2>],
270
+ selector: (values: [T1, T2]) => R,
271
+ equalityFn?: (a: R, b: R) => boolean
272
+ ): R;
273
+
274
+ // ... up to 5 nodes supported
275
+
276
+ function useSelect<T extends Record<string, DeepstateNode<unknown>>, R>(
277
+ nodes: T,
278
+ selector: (values: { [K in keyof T]: /* inferred */ }) => R,
279
+ equalityFn?: (a: R, b: R) => boolean
280
+ ): R;
281
+
282
+ // usePipeSelect
283
+ function usePipeSelect<T>(piped$: Observable<T>): T | undefined;
284
+
285
+ // useObservable
286
+ function useObservable<T>(
287
+ observable$: Observable<T>,
288
+ getSnapshot: () => T
289
+ ): T;
290
+ ```
291
+
292
+ ## Common Patterns
293
+
294
+ ### Debounced Search Input
295
+
296
+ ```tsx
297
+ function SearchBox() {
298
+ // Controlled input - immediate updates
299
+ const rawQuery = useSelect(store.searchQuery);
300
+
301
+ // Debounced for expensive operations
302
+ const debouncedQuery = usePipeSelect(
303
+ store.searchQuery.pipe(debounceTime(300))
304
+ );
305
+
306
+ return (
307
+ <div>
308
+ <input
309
+ value={rawQuery}
310
+ onChange={e => store.searchQuery.set(e.target.value)}
311
+ />
312
+ {debouncedQuery !== undefined && (
313
+ <SearchResults query={debouncedQuery} />
314
+ )}
315
+ </div>
316
+ );
317
+ }
318
+ ```
319
+
320
+ ### Computing Totals
321
+
322
+ ```tsx
323
+ function CartTotal() {
324
+ const total = usePipeSelect(
325
+ store.cart.items.pipe(
326
+ map(items => items.reduce((sum, i) => sum + i.price * i.qty, 0))
327
+ )
328
+ );
329
+
330
+ return <span>${(total ?? 0).toFixed(2)}</span>;
331
+ }
332
+ ```
333
+
334
+ ### Conditional Rendering
335
+
336
+ ```tsx
337
+ function ValidUser() {
338
+ const user = usePipeSelect(
339
+ store.user.pipe(filter(u => u.name.length > 0))
340
+ );
341
+
342
+ if (user === undefined) {
343
+ return <span>Please enter your name</span>;
344
+ }
345
+
346
+ return <Profile user={user} />;
347
+ }
348
+ ```
349
+
350
+ ### Preventing Re-renders
351
+
352
+ ```tsx
353
+ // Only re-render when age changes, not name
354
+ function UserAge() {
355
+ const age = useSelect(store.user, u => u.age);
356
+ return <span>{age}</span>;
357
+ }
358
+
359
+ // Or subscribe directly to the property
360
+ function UserAge() {
361
+ const age = useSelect(store.user.age);
362
+ return <span>{age}</span>;
363
+ }
364
+ ```
365
+
366
+ ## Peer Dependencies
367
+
368
+ - `react` ^18 || ^19
369
+ - `rxjs` ^7
370
+ - `@montra-interactive/deepstate` ^0.2.0
371
+
372
+ ## License
373
+
374
+ MIT
package/dist/hooks.d.ts CHANGED
@@ -1,15 +1,16 @@
1
1
  import type { Observable } from "rxjs";
2
2
  /**
3
- * Type helper to extract the value type from an Observable.
4
- * Works with deepstate nodes since they extend Observable.
3
+ * Interface for deepstate nodes that have a synchronous get() method.
4
+ * This is used internally to detect deepstate nodes vs plain observables.
5
5
  */
6
- type ObservableValue<T> = T extends Observable<infer V> ? V : never;
6
+ interface NodeWithGet<T> {
7
+ get(): T;
8
+ }
7
9
  /**
8
- * Type for object of observables -> object of their values
10
+ * A deepstate node - an Observable that also has a synchronous get() method.
11
+ * Used to enforce that useSelect only accepts deepstate nodes, not piped observables.
9
12
  */
10
- type ObservableObjectValues<T extends Record<string, Observable<unknown>>> = {
11
- [K in keyof T]: ObservableValue<T[K]>;
12
- };
13
+ export type DeepstateNode<T> = Observable<T> & NodeWithGet<T>;
13
14
  /**
14
15
  * Hook to subscribe to any Observable and get its current value.
15
16
  * Re-renders the component whenever the observable emits a new value.
@@ -118,13 +119,15 @@ export declare function useObservable<T>(observable$: Observable<T>, getSnapshot
118
119
  * }
119
120
  * ```
120
121
  */
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;
122
+ export declare function useSelect<T>(node: DeepstateNode<T>): T;
123
+ export declare function useSelect<T, R>(node: DeepstateNode<T>, selector: (value: T) => R, equalityFn?: (a: R, b: R) => boolean): R;
124
+ export declare function useSelect<T1, T2, R>(nodes: [DeepstateNode<T1>, DeepstateNode<T2>], selector: (values: [T1, T2]) => R, equalityFn?: (a: R, b: R) => boolean): R;
125
+ export declare function useSelect<T1, T2, T3, R>(nodes: [DeepstateNode<T1>, DeepstateNode<T2>, DeepstateNode<T3>], selector: (values: [T1, T2, T3]) => R, equalityFn?: (a: R, b: R) => boolean): R;
126
+ export declare function useSelect<T1, T2, T3, T4, R>(nodes: [DeepstateNode<T1>, DeepstateNode<T2>, DeepstateNode<T3>, DeepstateNode<T4>], selector: (values: [T1, T2, T3, T4]) => R, equalityFn?: (a: R, b: R) => boolean): R;
127
+ export declare function useSelect<T1, T2, T3, T4, T5, R>(nodes: [DeepstateNode<T1>, DeepstateNode<T2>, DeepstateNode<T3>, DeepstateNode<T4>, DeepstateNode<T5>], selector: (values: [T1, T2, T3, T4, T5]) => R, equalityFn?: (a: R, b: R) => boolean): R;
128
+ export declare function useSelect<T extends Record<string, DeepstateNode<unknown>>, R>(nodes: T, selector: (values: {
129
+ [K in keyof T]: T[K] extends DeepstateNode<infer V> ? V : never;
130
+ }) => R, equalityFn?: (a: R, b: R) => boolean): R;
128
131
  /**
129
132
  * @deprecated Use `useSelect` instead. This is an alias for backwards compatibility.
130
133
  */
@@ -133,5 +136,79 @@ export declare const useStateValue: typeof useSelect;
133
136
  * @deprecated Use `useSelect` instead. This is an alias for backwards compatibility.
134
137
  */
135
138
  export declare const useSelector: typeof useSelect;
139
+ /**
140
+ * Hook to subscribe to a piped observable stream.
141
+ * Unlike `useSelect`, this hook is designed for observables that have been transformed
142
+ * with RxJS operators like `filter`, `debounceTime`, `map`, etc.
143
+ *
144
+ * Since piped observables don't have a synchronous `.get()` method, the initial value
145
+ * is `undefined` until the first emission occurs.
146
+ *
147
+ * @param piped$ - An RxJS Observable (typically created by calling .pipe() on a deepstate node)
148
+ * @returns The current value from the stream, or `undefined` if no value has been emitted yet
149
+ *
150
+ * @example Basic usage with filter
151
+ * ```tsx
152
+ * import { usePipeSelect } from '@montra-interactive/deepstate-react';
153
+ * import { filter } from 'rxjs';
154
+ *
155
+ * function OnlyPositive() {
156
+ * // Will be undefined until a value > 0 is emitted
157
+ * const value = usePipeSelect(store.count.pipe(filter(v => v > 0)));
158
+ *
159
+ * if (value === undefined) {
160
+ * return <span>Waiting for positive value...</span>;
161
+ * }
162
+ * return <span>{value}</span>;
163
+ * }
164
+ * ```
165
+ *
166
+ * @example Debouncing high-frequency updates
167
+ * ```tsx
168
+ * import { usePipeSelect } from '@montra-interactive/deepstate-react';
169
+ * import { debounceTime } from 'rxjs';
170
+ *
171
+ * function DebouncedInput() {
172
+ * // Reduces re-renders by debouncing updates
173
+ * const searchTerm = usePipeSelect(store.searchInput.pipe(debounceTime(300)));
174
+ *
175
+ * return <span>Searching for: {searchTerm ?? 'nothing yet'}</span>;
176
+ * }
177
+ * ```
178
+ *
179
+ * @example Mapping values
180
+ * ```tsx
181
+ * import { usePipeSelect } from '@montra-interactive/deepstate-react';
182
+ * import { map } from 'rxjs';
183
+ *
184
+ * function ItemCount() {
185
+ * const count = usePipeSelect(store.items.pipe(map(items => items.length)));
186
+ *
187
+ * return <span>Count: {count ?? 0}</span>;
188
+ * }
189
+ * ```
190
+ *
191
+ * @example Combining operators
192
+ * ```tsx
193
+ * import { usePipeSelect } from '@montra-interactive/deepstate-react';
194
+ * import { filter, debounceTime, distinctUntilChanged } from 'rxjs';
195
+ *
196
+ * function FilteredSearch() {
197
+ * const results = usePipeSelect(
198
+ * store.searchResults.pipe(
199
+ * filter(r => r.length > 0),
200
+ * debounceTime(200),
201
+ * distinctUntilChanged()
202
+ * )
203
+ * );
204
+ *
205
+ * if (results === undefined) {
206
+ * return <span>No results yet</span>;
207
+ * }
208
+ * return <ul>{results.map(r => <li key={r.id}>{r.name}</li>)}</ul>;
209
+ * }
210
+ * ```
211
+ */
212
+ export declare function usePipeSelect<T>(piped$: Observable<T>): T | undefined;
136
213
  export {};
137
214
  //# sourceMappingURL=hooks.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -32,6 +32,7 @@
32
32
  * }
33
33
  * ```
34
34
  */
35
- export { useSelect, useObservable, useStateValue, useSelector, } from "./hooks";
35
+ export { useSelect, usePipeSelect, useObservable, useStateValue, useSelector, } from "./hooks";
36
+ export type { DeepstateNode } from "./hooks";
36
37
  export type { Observable } from "rxjs";
37
38
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EACL,SAAS,EACT,aAAa,EACb,aAAa,EAEb,aAAa,EACb,WAAW,GACZ,MAAM,SAAS,CAAC;AAEjB,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C,YAAY,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC"}
package/dist/index.js CHANGED
@@ -104,9 +104,24 @@ function useSelect(nodeOrNodes, selector, equalityFn = Object.is) {
104
104
  }
105
105
  var useStateValue = useSelect;
106
106
  var useSelector = useSelect;
107
+ function usePipeSelect(piped$) {
108
+ const hasValueRef = useRef(false);
109
+ const valueRef = useRef(undefined);
110
+ const subscribe = useCallback((onStoreChange) => {
111
+ const subscription = piped$.subscribe((newValue) => {
112
+ hasValueRef.current = true;
113
+ valueRef.current = newValue;
114
+ onStoreChange();
115
+ });
116
+ return () => subscription.unsubscribe();
117
+ }, [piped$]);
118
+ const getSnapshot = useCallback(() => valueRef.current, []);
119
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
120
+ }
107
121
  export {
108
122
  useStateValue,
109
123
  useSelector,
110
124
  useSelect,
125
+ usePipeSelect,
111
126
  useObservable
112
127
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@montra-interactive/deepstate-react",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
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
@@ -31,6 +31,12 @@ interface NodeWithGet<T> {
31
31
  get(): T;
32
32
  }
33
33
 
34
+ /**
35
+ * A deepstate node - an Observable that also has a synchronous get() method.
36
+ * Used to enforce that useSelect only accepts deepstate nodes, not piped observables.
37
+ */
38
+ export type DeepstateNode<T> = Observable<T> & NodeWithGet<T>;
39
+
34
40
  function hasGet<T>(obj: unknown): obj is NodeWithGet<T> {
35
41
  if (obj === null || typeof obj !== "object") return false;
36
42
  // Check by accessing get directly - works with proxied observables
@@ -184,65 +190,45 @@ export function useObservable<T>(
184
190
  * ```
185
191
  */
186
192
  // Single node, no selector - return raw value
187
- export function useSelect<T extends Observable<unknown>>(
188
- node: T
189
- ): ObservableValue<T>;
193
+ // Note: Requires a deepstate node (with .get()), not a piped observable.
194
+ // Use usePipeSelect for piped observables.
195
+ export function useSelect<T>(
196
+ node: DeepstateNode<T>
197
+ ): T;
190
198
  // Single node with selector
191
- export function useSelect<T extends Observable<unknown>, R>(
192
- node: T,
193
- selector: (value: ObservableValue<T>) => R,
199
+ export function useSelect<T, R>(
200
+ node: DeepstateNode<T>,
201
+ selector: (value: T) => R,
194
202
  equalityFn?: (a: R, b: R) => boolean
195
203
  ): R;
196
204
  // Array of 2 nodes with selector
197
- export function useSelect<
198
- T1 extends Observable<unknown>,
199
- T2 extends Observable<unknown>,
200
- R
201
- >(
202
- nodes: [T1, T2],
203
- selector: (values: [ObservableValue<T1>, ObservableValue<T2>]) => R,
205
+ export function useSelect<T1, T2, R>(
206
+ nodes: [DeepstateNode<T1>, DeepstateNode<T2>],
207
+ selector: (values: [T1, T2]) => R,
204
208
  equalityFn?: (a: R, b: R) => boolean
205
209
  ): R;
206
210
  // Array of 3 nodes with selector
207
- export function useSelect<
208
- T1 extends Observable<unknown>,
209
- T2 extends Observable<unknown>,
210
- T3 extends Observable<unknown>,
211
- R
212
- >(
213
- nodes: [T1, T2, T3],
214
- selector: (values: [ObservableValue<T1>, ObservableValue<T2>, ObservableValue<T3>]) => R,
211
+ export function useSelect<T1, T2, T3, R>(
212
+ nodes: [DeepstateNode<T1>, DeepstateNode<T2>, DeepstateNode<T3>],
213
+ selector: (values: [T1, T2, T3]) => R,
215
214
  equalityFn?: (a: R, b: R) => boolean
216
215
  ): R;
217
216
  // Array of 4 nodes with selector
218
- export function useSelect<
219
- T1 extends Observable<unknown>,
220
- T2 extends Observable<unknown>,
221
- T3 extends Observable<unknown>,
222
- T4 extends Observable<unknown>,
223
- R
224
- >(
225
- nodes: [T1, T2, T3, T4],
226
- selector: (values: [ObservableValue<T1>, ObservableValue<T2>, ObservableValue<T3>, ObservableValue<T4>]) => R,
217
+ export function useSelect<T1, T2, T3, T4, R>(
218
+ nodes: [DeepstateNode<T1>, DeepstateNode<T2>, DeepstateNode<T3>, DeepstateNode<T4>],
219
+ selector: (values: [T1, T2, T3, T4]) => R,
227
220
  equalityFn?: (a: R, b: R) => boolean
228
221
  ): R;
229
222
  // Array of 5 nodes with selector
230
- export function useSelect<
231
- T1 extends Observable<unknown>,
232
- T2 extends Observable<unknown>,
233
- T3 extends Observable<unknown>,
234
- T4 extends Observable<unknown>,
235
- T5 extends Observable<unknown>,
236
- R
237
- >(
238
- nodes: [T1, T2, T3, T4, T5],
239
- selector: (values: [ObservableValue<T1>, ObservableValue<T2>, ObservableValue<T3>, ObservableValue<T4>, ObservableValue<T5>]) => R,
223
+ export function useSelect<T1, T2, T3, T4, T5, R>(
224
+ nodes: [DeepstateNode<T1>, DeepstateNode<T2>, DeepstateNode<T3>, DeepstateNode<T4>, DeepstateNode<T5>],
225
+ selector: (values: [T1, T2, T3, T4, T5]) => R,
240
226
  equalityFn?: (a: R, b: R) => boolean
241
227
  ): R;
242
228
  // Object of nodes with selector
243
- export function useSelect<T extends Record<string, Observable<unknown>>, R>(
229
+ export function useSelect<T extends Record<string, DeepstateNode<unknown>>, R>(
244
230
  nodes: T,
245
- selector: (values: ObservableObjectValues<T>) => R,
231
+ selector: (values: { [K in keyof T]: T[K] extends DeepstateNode<infer V> ? V : never }) => R,
246
232
  equalityFn?: (a: R, b: R) => boolean
247
233
  ): R;
248
234
  // Implementation
@@ -364,3 +350,99 @@ export const useStateValue = useSelect;
364
350
  * @deprecated Use `useSelect` instead. This is an alias for backwards compatibility.
365
351
  */
366
352
  export const useSelector = useSelect;
353
+
354
+ /**
355
+ * Hook to subscribe to a piped observable stream.
356
+ * Unlike `useSelect`, this hook is designed for observables that have been transformed
357
+ * with RxJS operators like `filter`, `debounceTime`, `map`, etc.
358
+ *
359
+ * Since piped observables don't have a synchronous `.get()` method, the initial value
360
+ * is `undefined` until the first emission occurs.
361
+ *
362
+ * @param piped$ - An RxJS Observable (typically created by calling .pipe() on a deepstate node)
363
+ * @returns The current value from the stream, or `undefined` if no value has been emitted yet
364
+ *
365
+ * @example Basic usage with filter
366
+ * ```tsx
367
+ * import { usePipeSelect } from '@montra-interactive/deepstate-react';
368
+ * import { filter } from 'rxjs';
369
+ *
370
+ * function OnlyPositive() {
371
+ * // Will be undefined until a value > 0 is emitted
372
+ * const value = usePipeSelect(store.count.pipe(filter(v => v > 0)));
373
+ *
374
+ * if (value === undefined) {
375
+ * return <span>Waiting for positive value...</span>;
376
+ * }
377
+ * return <span>{value}</span>;
378
+ * }
379
+ * ```
380
+ *
381
+ * @example Debouncing high-frequency updates
382
+ * ```tsx
383
+ * import { usePipeSelect } from '@montra-interactive/deepstate-react';
384
+ * import { debounceTime } from 'rxjs';
385
+ *
386
+ * function DebouncedInput() {
387
+ * // Reduces re-renders by debouncing updates
388
+ * const searchTerm = usePipeSelect(store.searchInput.pipe(debounceTime(300)));
389
+ *
390
+ * return <span>Searching for: {searchTerm ?? 'nothing yet'}</span>;
391
+ * }
392
+ * ```
393
+ *
394
+ * @example Mapping values
395
+ * ```tsx
396
+ * import { usePipeSelect } from '@montra-interactive/deepstate-react';
397
+ * import { map } from 'rxjs';
398
+ *
399
+ * function ItemCount() {
400
+ * const count = usePipeSelect(store.items.pipe(map(items => items.length)));
401
+ *
402
+ * return <span>Count: {count ?? 0}</span>;
403
+ * }
404
+ * ```
405
+ *
406
+ * @example Combining operators
407
+ * ```tsx
408
+ * import { usePipeSelect } from '@montra-interactive/deepstate-react';
409
+ * import { filter, debounceTime, distinctUntilChanged } from 'rxjs';
410
+ *
411
+ * function FilteredSearch() {
412
+ * const results = usePipeSelect(
413
+ * store.searchResults.pipe(
414
+ * filter(r => r.length > 0),
415
+ * debounceTime(200),
416
+ * distinctUntilChanged()
417
+ * )
418
+ * );
419
+ *
420
+ * if (results === undefined) {
421
+ * return <span>No results yet</span>;
422
+ * }
423
+ * return <ul>{results.map(r => <li key={r.id}>{r.name}</li>)}</ul>;
424
+ * }
425
+ * ```
426
+ */
427
+ export function usePipeSelect<T>(piped$: Observable<T>): T | undefined {
428
+ // Track whether we've received a value yet
429
+ const hasValueRef = useRef(false);
430
+ const valueRef = useRef<T | undefined>(undefined);
431
+
432
+ const subscribe = useCallback(
433
+ (onStoreChange: () => void) => {
434
+ const subscription = piped$.subscribe((newValue) => {
435
+ hasValueRef.current = true;
436
+ valueRef.current = newValue;
437
+ onStoreChange();
438
+ });
439
+
440
+ return () => subscription.unsubscribe();
441
+ },
442
+ [piped$]
443
+ );
444
+
445
+ const getSnapshot = useCallback(() => valueRef.current, []);
446
+
447
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
448
+ }
package/src/index.ts CHANGED
@@ -35,10 +35,13 @@
35
35
 
36
36
  export {
37
37
  useSelect,
38
+ usePipeSelect,
38
39
  useObservable,
39
40
  // Deprecated aliases for backwards compatibility
40
41
  useStateValue,
41
42
  useSelector,
42
43
  } from "./hooks";
43
44
 
45
+ export type { DeepstateNode } from "./hooks";
46
+
44
47
  export type { Observable } from "rxjs";