@montra-interactive/deepstate-react 0.3.5 → 0.3.6-alpha.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.
@@ -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;AAwBvC;;;GAGG;AACH,UAAU,WAAW,CAAC,CAAC;IACrB,GAAG,IAAI,CAAC,CAAC;CACV;AAED;;;GAGG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;AAwB9D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAC7B,WAAW,EAAE,UAAU,CAAC,CAAC,CAAC,EAC1B,WAAW,EAAE,MAAM,CAAC,GACnB,CAAC,CAkBH;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmFG;AAIH,wBAAgB,SAAS,CAAC,CAAC,EACzB,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,GACrB,CAAC,CAAC;AAEL,wBAAgB,SAAS,CAAC,CAAC,EAAE,CAAC,EAC5B,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,EACtB,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,EACzB,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,GACnC,CAAC,CAAC;AAEL,wBAAgB,SAAS,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,EACjC,KAAK,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC,EAAE,aAAa,CAAC,EAAE,CAAC,CAAC,EAC7C,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,EACjC,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,GACnC,CAAC,CAAC;AAEL,wBAAgB,SAAS,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EACrC,KAAK,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC,EAAE,aAAa,CAAC,EAAE,CAAC,EAAE,aAAa,CAAC,EAAE,CAAC,CAAC,EAChE,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,EACrC,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,GACnC,CAAC,CAAC;AAEL,wBAAgB,SAAS,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EACzC,KAAK,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC,EAAE,aAAa,CAAC,EAAE,CAAC,EAAE,aAAa,CAAC,EAAE,CAAC,EAAE,aAAa,CAAC,EAAE,CAAC,CAAC,EACnF,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,EACzC,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,GACnC,CAAC,CAAC;AAEL,wBAAgB,SAAS,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAC7C,KAAK,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC,EAAE,aAAa,CAAC,EAAE,CAAC,EAAE,aAAa,CAAC,EAAE,CAAC,EAAE,aAAa,CAAC,EAAE,CAAC,EAAE,aAAa,CAAC,EAAE,CAAC,CAAC,EACtG,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,EAC7C,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,GACnC,CAAC,CAAC;AAEL,wBAAgB,SAAS,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,EAC3E,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,CAAC,MAAM,EAAE;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,aAAa,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK;CAAE,KAAK,CAAC,EAC5F,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,GACnC,CAAC,CAAC;AA+GL;;GAEG;AACH,eAAO,MAAM,aAAa,kBAAY,CAAC;AAEvC;;GAEG;AACH,eAAO,MAAM,WAAW,kBAAY,CAAC;AAErC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwEG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,SAAS,CAqBrE"}
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -20,6 +20,34 @@ function isObservable(obj) {
20
20
  return false;
21
21
  }
22
22
  }
23
+ function useStableNodes(nodeOrNodes) {
24
+ const ref = useRef(nodeOrNodes);
25
+ if (Array.isArray(nodeOrNodes)) {
26
+ const prev = ref.current;
27
+ if (!Array.isArray(prev) || prev.length !== nodeOrNodes.length || nodeOrNodes.some((n, i) => n !== prev[i])) {
28
+ ref.current = nodeOrNodes;
29
+ }
30
+ return ref.current;
31
+ }
32
+ if (!isObservable(nodeOrNodes)) {
33
+ const prev = ref.current;
34
+ if (isObservable(prev) || Array.isArray(prev)) {
35
+ ref.current = nodeOrNodes;
36
+ return ref.current;
37
+ }
38
+ const prevObj = prev;
39
+ const currKeys = Object.keys(nodeOrNodes);
40
+ const prevKeys = Object.keys(prevObj);
41
+ if (currKeys.length !== prevKeys.length || currKeys.some((k) => nodeOrNodes[k] !== prevObj[k])) {
42
+ ref.current = nodeOrNodes;
43
+ }
44
+ return ref.current;
45
+ }
46
+ if (nodeOrNodes !== ref.current) {
47
+ ref.current = nodeOrNodes;
48
+ }
49
+ return ref.current;
50
+ }
23
51
  function useObservable(observable$, getSnapshot) {
24
52
  const valueRef = useRef(getSnapshot());
25
53
  const subscribe = useCallback((onStoreChange) => {
@@ -33,55 +61,58 @@ function useObservable(observable$, getSnapshot) {
33
61
  return useSyncExternalStore(subscribe, getSnapshotMemo, getSnapshotMemo);
34
62
  }
35
63
  function useSelect(nodeOrNodes, selector, equalityFn = Object.is) {
64
+ const selectorRef = useRef(selector);
65
+ selectorRef.current = selector;
66
+ const equalityFnRef = useRef(equalityFn);
67
+ equalityFnRef.current = equalityFn;
68
+ const stableNodes = useStableNodes(nodeOrNodes);
36
69
  const { combined$, getInitialValue } = useMemo(() => {
37
- if (Array.isArray(nodeOrNodes)) {
38
- const nodes = nodeOrNodes;
39
- const sel = selector;
70
+ if (Array.isArray(stableNodes)) {
71
+ const nodes = stableNodes;
40
72
  return {
41
- combined$: combineLatest(nodes).pipe(map((values) => sel(values)), distinctUntilChanged(equalityFn)),
73
+ combined$: combineLatest(nodes).pipe(distinctUntilChanged((a, b) => a.length === b.length && a.every((v, i) => Object.is(v, b[i]))), map((values) => selectorRef.current(values)), distinctUntilChanged((a, b) => equalityFnRef.current(a, b))),
42
74
  getInitialValue: () => {
43
75
  const values = nodes.map((n) => hasGet(n) ? n.get() : undefined);
44
- return sel(values);
76
+ return selectorRef.current(values);
45
77
  }
46
78
  };
47
79
  }
48
- if (!isObservable(nodeOrNodes)) {
49
- const obj = nodeOrNodes;
80
+ if (!isObservable(stableNodes)) {
81
+ const obj = stableNodes;
50
82
  const keys = Object.keys(obj);
51
83
  const observables = keys.map((k) => obj[k]);
52
- const sel = selector;
53
84
  return {
54
- combined$: combineLatest(observables).pipe(map((values) => {
85
+ combined$: combineLatest(observables).pipe(distinctUntilChanged((a, b) => a.length === b.length && a.every((v, i) => Object.is(v, b[i]))), map((values) => {
55
86
  const result = {};
56
87
  keys.forEach((key, i) => {
57
88
  result[key] = values[i];
58
89
  });
59
- return sel(result);
60
- }), distinctUntilChanged(equalityFn)),
90
+ return selectorRef.current(result);
91
+ }), distinctUntilChanged((a, b) => equalityFnRef.current(a, b))),
61
92
  getInitialValue: () => {
62
93
  const result = {};
63
94
  keys.forEach((key) => {
64
95
  const node2 = obj[key];
65
96
  result[key] = hasGet(node2) ? node2.get() : undefined;
66
97
  });
67
- return sel(result);
98
+ return selectorRef.current(result);
68
99
  }
69
100
  };
70
101
  }
71
- const node = nodeOrNodes;
72
- if (selector) {
102
+ const node = stableNodes;
103
+ if (selectorRef.current) {
73
104
  return {
74
- combined$: node.pipe(map((value) => selector(value)), distinctUntilChanged(equalityFn)),
105
+ combined$: node.pipe(distinctUntilChanged(), map((value) => selectorRef.current(value)), distinctUntilChanged((a, b) => equalityFnRef.current(a, b))),
75
106
  getInitialValue: () => {
76
107
  if (hasGet(node)) {
77
- return selector(node.get());
108
+ return selectorRef.current(node.get());
78
109
  }
79
110
  return;
80
111
  }
81
112
  };
82
113
  } else {
83
114
  return {
84
- combined$: node.pipe(distinctUntilChanged(equalityFn)),
115
+ combined$: node.pipe(distinctUntilChanged((a, b) => equalityFnRef.current(a, b))),
85
116
  getInitialValue: () => {
86
117
  if (hasGet(node)) {
87
118
  return node.get();
@@ -90,7 +121,7 @@ function useSelect(nodeOrNodes, selector, equalityFn = Object.is) {
90
121
  }
91
122
  };
92
123
  }
93
- }, [nodeOrNodes, selector, equalityFn]);
124
+ }, [stableNodes]);
94
125
  const valueRef = useRef(getInitialValue());
95
126
  const subscribe = useCallback((onStoreChange) => {
96
127
  const subscription = combined$.subscribe((newValue) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@montra-interactive/deepstate-react",
3
- "version": "0.3.5",
3
+ "version": "0.3.6-alpha.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
@@ -59,6 +59,62 @@ function isObservable(obj: unknown): obj is Observable<unknown> {
59
59
  }
60
60
  }
61
61
 
62
+ /**
63
+ * Stabilizes the identity of nodeOrNodes across re-renders.
64
+ *
65
+ * Users typically pass inline arrays like `[store.a, store.b]` or inline objects
66
+ * like `{ a: store.a }` to useSelect. These are new references every render,
67
+ * which would cause useMemo to recreate the observable pipeline, leading to
68
+ * resubscription and potential infinite render loops (shareReplay replays the
69
+ * last value → onStoreChange → re-render → new useMemo → resubscribe → replay → …).
70
+ *
71
+ * This hook compares the individual node references inside the container and
72
+ * returns a stable reference as long as the nodes themselves haven't changed.
73
+ */
74
+ type NodeInput = Observable<unknown> | Observable<unknown>[] | Record<string, Observable<unknown>>;
75
+
76
+ function useStableNodes(nodeOrNodes: NodeInput): NodeInput {
77
+ const ref = useRef(nodeOrNodes);
78
+
79
+ // For arrays: check element-wise identity
80
+ if (Array.isArray(nodeOrNodes)) {
81
+ const prev = ref.current;
82
+ if (
83
+ !Array.isArray(prev) ||
84
+ prev.length !== nodeOrNodes.length ||
85
+ nodeOrNodes.some((n, i) => n !== (prev as Observable<unknown>[])[i])
86
+ ) {
87
+ ref.current = nodeOrNodes;
88
+ }
89
+ return ref.current;
90
+ }
91
+
92
+ // For objects (not observable): check key-wise identity
93
+ if (!isObservable(nodeOrNodes)) {
94
+ const prev = ref.current;
95
+ if (isObservable(prev) || Array.isArray(prev)) {
96
+ ref.current = nodeOrNodes;
97
+ return ref.current;
98
+ }
99
+ const prevObj = prev as Record<string, Observable<unknown>>;
100
+ const currKeys = Object.keys(nodeOrNodes);
101
+ const prevKeys = Object.keys(prevObj);
102
+ if (
103
+ currKeys.length !== prevKeys.length ||
104
+ currKeys.some((k) => nodeOrNodes[k] !== prevObj[k])
105
+ ) {
106
+ ref.current = nodeOrNodes;
107
+ }
108
+ return ref.current;
109
+ }
110
+
111
+ // For single observable: direct identity check
112
+ if (nodeOrNodes !== ref.current) {
113
+ ref.current = nodeOrNodes;
114
+ }
115
+ return ref.current;
116
+ }
117
+
62
118
  /**
63
119
  * Hook to subscribe to any Observable and get its current value.
64
120
  * Re-renders the component whenever the observable emits a new value.
@@ -239,41 +295,62 @@ export function useSelect(
239
295
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
240
296
  equalityFn: (a: any, b: any) => boolean = Object.is
241
297
  ): unknown {
298
+ // Use refs for selector and equalityFn so we always call the latest version
299
+ // without needing them as useMemo deps (which would recreate the observable).
300
+ const selectorRef = useRef(selector);
301
+ selectorRef.current = selector;
302
+ const equalityFnRef = useRef(equalityFn);
303
+ equalityFnRef.current = equalityFn;
304
+
305
+ // Stabilize the node identity across renders.
306
+ // Users pass inline arrays/objects like [store.a, store.b] or { a: store.a },
307
+ // which are new references each render. We extract the actual node references
308
+ // and only recreate the observable when the nodes themselves change.
309
+ const stableNodes = useStableNodes(nodeOrNodes);
310
+
242
311
  // Determine the form and create the combined observable
243
312
  const { combined$, getInitialValue } = useMemo(() => {
244
313
  // Array form: [node1, node2, ...] - always requires selector
245
- if (Array.isArray(nodeOrNodes)) {
246
- const nodes = nodeOrNodes as Observable<unknown>[];
247
- const sel = selector!; // selector is required for array form
314
+ if (Array.isArray(stableNodes)) {
315
+ const nodes = stableNodes as Observable<unknown>[];
248
316
  return {
249
317
  combined$: combineLatest(nodes).pipe(
250
- map((values) => sel(values)),
251
- distinctUntilChanged(equalityFn)
318
+ // Deduplicate inputs so the selector only re-runs when an input actually changes.
319
+ // This prevents selectors that return new references (e.g. .sort(), .map())
320
+ // from causing infinite emission loops with the default Object.is equality.
321
+ distinctUntilChanged((a, b) =>
322
+ a.length === b.length && a.every((v, i) => Object.is(v, b[i]))
323
+ ),
324
+ map((values) => selectorRef.current!(values)),
325
+ distinctUntilChanged((a, b) => equalityFnRef.current(a, b))
252
326
  ),
253
327
  getInitialValue: (): unknown => {
254
328
  const values = nodes.map((n) => (hasGet<unknown>(n) ? n.get() : undefined));
255
- return sel(values);
329
+ return selectorRef.current!(values);
256
330
  },
257
331
  };
258
332
  }
259
333
 
260
334
  // Object form: { a: node1, b: node2, ... } - always requires selector
261
- if (!isObservable(nodeOrNodes)) {
262
- const obj = nodeOrNodes as Record<string, Observable<unknown>>;
335
+ if (!isObservable(stableNodes)) {
336
+ const obj = stableNodes as Record<string, Observable<unknown>>;
263
337
  const keys = Object.keys(obj);
264
338
  const observables = keys.map((k) => obj[k]);
265
- const sel = selector!; // selector is required for object form
266
339
 
267
340
  return {
268
341
  combined$: combineLatest(observables).pipe(
342
+ // Deduplicate inputs so the selector only re-runs when an input actually changes.
343
+ distinctUntilChanged((a, b) =>
344
+ a.length === b.length && a.every((v, i) => Object.is(v, b[i]))
345
+ ),
269
346
  map((values) => {
270
347
  const result: Record<string, unknown> = {};
271
348
  keys.forEach((key, i) => {
272
349
  result[key] = values[i];
273
350
  });
274
- return sel(result);
351
+ return selectorRef.current!(result);
275
352
  }),
276
- distinctUntilChanged(equalityFn)
353
+ distinctUntilChanged((a, b) => equalityFnRef.current(a, b))
277
354
  ),
278
355
  getInitialValue: (): unknown => {
279
356
  const result: Record<string, unknown> = {};
@@ -281,24 +358,26 @@ export function useSelect(
281
358
  const node = obj[key];
282
359
  result[key] = hasGet<unknown>(node) ? node.get() : undefined;
283
360
  });
284
- return sel(result);
361
+ return selectorRef.current!(result);
285
362
  },
286
363
  };
287
364
  }
288
365
 
289
366
  // Single node form - selector is optional
290
- const node = nodeOrNodes as Observable<unknown>;
291
-
292
- if (selector) {
367
+ const node = stableNodes as Observable<unknown>;
368
+
369
+ if (selectorRef.current) {
293
370
  // With selector - apply transformation
294
371
  return {
295
372
  combined$: node.pipe(
296
- map((value) => selector(value)),
297
- distinctUntilChanged(equalityFn)
373
+ // Deduplicate inputs so the selector only re-runs when the input actually changes.
374
+ distinctUntilChanged(),
375
+ map((value) => selectorRef.current!(value)),
376
+ distinctUntilChanged((a, b) => equalityFnRef.current(a, b))
298
377
  ),
299
378
  getInitialValue: (): unknown => {
300
379
  if (hasGet<unknown>(node)) {
301
- return selector(node.get());
380
+ return selectorRef.current!(node.get());
302
381
  }
303
382
  return undefined;
304
383
  },
@@ -307,7 +386,7 @@ export function useSelect(
307
386
  // No selector - return raw value
308
387
  return {
309
388
  combined$: node.pipe(
310
- distinctUntilChanged(equalityFn)
389
+ distinctUntilChanged((a, b) => equalityFnRef.current(a, b))
311
390
  ),
312
391
  getInitialValue: (): unknown => {
313
392
  if (hasGet<unknown>(node)) {
@@ -317,7 +396,8 @@ export function useSelect(
317
396
  },
318
397
  };
319
398
  }
320
- }, [nodeOrNodes, selector, equalityFn]);
399
+ // eslint-disable-next-line react-hooks/exhaustive-deps
400
+ }, [stableNodes]);
321
401
 
322
402
  // Ref to hold the current derived value
323
403
  const valueRef = useRef<unknown>(getInitialValue());