@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 +374 -0
- package/dist/hooks.d.ts +91 -14
- package/dist/hooks.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -0
- package/package.json +1 -1
- package/src/hooks.ts +124 -42
- package/src/index.ts +3 -0
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
|
-
*
|
|
4
|
-
*
|
|
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
|
-
|
|
6
|
+
interface NodeWithGet<T> {
|
|
7
|
+
get(): T;
|
|
8
|
+
}
|
|
7
9
|
/**
|
|
8
|
-
*
|
|
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
|
|
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
|
|
122
|
-
export declare function useSelect<T
|
|
123
|
-
export declare function useSelect<T1
|
|
124
|
-
export declare function useSelect<T1
|
|
125
|
-
export declare function useSelect<T1
|
|
126
|
-
export declare function useSelect<T1
|
|
127
|
-
export declare function useSelect<T extends Record<string,
|
|
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
|
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
|
@@ -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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
192
|
-
node: T
|
|
193
|
-
selector: (value:
|
|
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
|
|
199
|
-
T2
|
|
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
|
|
209
|
-
T2
|
|
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
|
|
220
|
-
T2
|
|
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
|
|
232
|
-
T2
|
|
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,
|
|
229
|
+
export function useSelect<T extends Record<string, DeepstateNode<unknown>>, R>(
|
|
244
230
|
nodes: T,
|
|
245
|
-
selector: (values:
|
|
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";
|