react-deepwatch 1.0.0 → 1.1.1

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/index.ts CHANGED
@@ -4,15 +4,19 @@ import {
4
4
  RecordedValueRead,
5
5
  WatchedProxyFacade, installChangeTracker
6
6
  } from "proxy-facades";
7
- import {arraysAreEqualsByPredicateFn, isObject, PromiseState, recordedReadsArraysAreEqual, throwError} from "./Util";
7
+ import {
8
+ arraysAreEqualsByPredicateFn,
9
+ isObject,
10
+ newDefaultMap,
11
+ PromiseState,
12
+ recordedReadsArraysAreEqual,
13
+ throwError
14
+ } from "./Util";
8
15
  import {useLayoutEffect, useState, createElement, Fragment, ReactNode, useEffect, useContext, memo} from "react";
9
16
  import {ErrorBoundaryContext, useErrorBoundary} from "react-error-boundary";
10
17
  import {_preserve, preserve, PreserveOptions} from "./preserve";
11
18
 
12
- let watchedProxyFacade: WatchedProxyFacade | undefined
13
- function getWatchedProxyFacade() {
14
- return watchedProxyFacade || (watchedProxyFacade = new WatchedProxyFacade()); // Lazy initialize global variable
15
- }
19
+ let sharedWatchedProxyFacade: WatchedProxyFacade | undefined
16
20
 
17
21
  let debug_idGenerator=0;
18
22
 
@@ -34,12 +38,10 @@ type WatchedComponentOptions = {
34
38
  memo?: boolean
35
39
 
36
40
  /**
37
- * TODO
38
41
  * Normally, everything that's **taken** from props, {@link useWatchedState} or {@link watched} or load(...)'s result will be returned, wrapped in a proxy that watches for modifications.
39
- * So far, so good, this can handle all stuff that's happening inside your component, but the outside world does not have these proxies. For example, when a parent component is not a watchedComponent, and passed in an object (i.e. the model) into this component via props.
42
+ * So far, so good, this can handle all stuff that's happening **inside** your component, but the outside world does not have these proxies. For example, when a parent component is not a watchedComponent, and passed in an object (i.e. the model) into this component via props.
40
43
  * Therefore this component can also **patch** these objects to make them watchable.
41
44
  *
42
- *
43
45
  * <p>Default: true</p>
44
46
  */
45
47
  watchOutside?: boolean
@@ -57,6 +59,17 @@ type WatchedComponentOptions = {
57
59
  * </p>
58
60
  */
59
61
  preserveProps?: boolean
62
+
63
+ /**
64
+ * Shares a global proxy facade instance for all watchedComponents. This is the more tested and easier to think about option.
65
+ * Disabling this creates a layer of new proxies for every of your child components. Object instances **inside** your component may still be consistent, but there could be: objInsideMyComponent !== myObjectHandedOverGloballyInSomeOtherWay. Real world use cases have to prove, if this is good way.
66
+ * Note:
67
+ * <p>
68
+ * Default: true
69
+ * </p>
70
+ */
71
+ // Development: Note: Also keep in mind: `this.proxyHandler === other.proxyHandler` in RecordedPropertyRead#equals -> is this a problem? I assume that the component instances/state and therefore the watchedProxyFacades stay the same.
72
+ useGlobalSharedProxyFacade?: boolean
60
73
  }
61
74
 
62
75
  /**
@@ -306,10 +319,20 @@ class LoadCall {
306
319
  class WatchedComponentPersistent {
307
320
  options: WatchedComponentOptions;
308
321
 
322
+ _nonSharedWatchedProxyFacade?: WatchedProxyFacade;
323
+
324
+ get watchedProxyFacade() {
325
+ if(this.options.useGlobalSharedProxyFacade === false) {
326
+ return this._nonSharedWatchedProxyFacade || (this._nonSharedWatchedProxyFacade = new WatchedProxyFacade());
327
+ }
328
+ // Use a global shared instance
329
+ return sharedWatchedProxyFacade || (sharedWatchedProxyFacade = new WatchedProxyFacade()); // Lazy initialize global variable
330
+ }
331
+
309
332
  /**
310
333
  * props of the component. These are saved here in the state (in a non changing object instance), so code inside load call can watch **shallow** props changes on it.
311
334
  */
312
- watchedProps = getWatchedProxyFacade().getProxyFor({});
335
+ watchedProps: {};
313
336
 
314
337
  /**
315
338
  * id -> loadCall. Null when there are multiple for that id
@@ -341,6 +364,12 @@ class WatchedComponentPersistent {
341
364
 
342
365
  onceOnEffectCleanupListeners: (()=>void)[] = [];
343
366
 
367
+ /**
368
+ * For the <a href="https://github.com/bogeeee/react-deepwatch/blob/main/readme.md#and-less-handing-onchange-listeners-to-child-components">"And less... handing onChange listeners to child components"</a> use case.
369
+ * We don't want new object instances created, every time watched(..., {onChange:....}) is called. So we assign the proxy facade (see watched function) to the object
370
+ */
371
+ object_to_childProxyFacade = newDefaultMap<object, WatchedProxyFacade>(() => new WatchedProxyFacade());
372
+
344
373
  debug_tag?: string;
345
374
 
346
375
  protected doReRender() {
@@ -396,6 +425,7 @@ class WatchedComponentPersistent {
396
425
 
397
426
  constructor(options: WatchedComponentOptions) {
398
427
  this.options = options;
428
+ this.watchedProps = this.watchedProxyFacade.getProxyFor({})
399
429
  }
400
430
 
401
431
  /**
@@ -449,12 +479,6 @@ class Frame {
449
479
 
450
480
  isListeningForChanges = false;
451
481
 
452
- //watchedProxyFacade= new WatchedProxyFacade();
453
- get watchedProxyFacade() {
454
- // Use a global shared instance. Because there's no exclusive state inside the graph/handlers. And state.someObj = state.someObj does not cause us multiple nesting layers of proxies. Still this may not the final choice. When changing this mind also the `this.proxyHandler === other.proxyHandler` in RecordedPropertyRead#equals
455
- return getWatchedProxyFacade();
456
- }
457
-
458
482
  constructor() {
459
483
  this.watchPropertyChange_changeListenerFn = this.watchPropertyChange_changeListenerFn.bind(this); // method is handed over as function but uses "this" inside.
460
484
  }
@@ -508,7 +532,12 @@ class Frame {
508
532
  }
509
533
  }
510
534
  catch (e) {
511
- throw new Error(`Could not enhance the original object to track reads. This can fail, if it was created with some unsupported language constructs (defining read only properties; subclassing Array, Set or Map; ...). You can switch it off via the WatchedComponentOptions#watchOutside flag. I.e: const MyComponent = watchedComponent(props => {...}, {watchOutside: false})`, {cause: e});
535
+ if((e as Error).message?.startsWith("Cannot install change tracker on a proxy")) { // Hacky way to catch this. TODO: Expose a proper API for it.
536
+
537
+ }
538
+ else {
539
+ throw new Error(`Could not enhance the original object to track reads. This can fail, if it was created with some unsupported language constructs (defining read only properties; subclassing Array, Set or Map; ...). You can switch it off via the WatchedComponentOptions#watchOutside flag. I.e: const MyComponent = watchedComponent(props => {...}, {watchOutside: false})`, {cause: e});
540
+ }
512
541
  }
513
542
  }
514
543
 
@@ -551,6 +580,18 @@ class RenderRun {
551
580
  somePending?: Promise<unknown>;
552
581
  somePendingAreCritical = false;
553
582
 
583
+ /**
584
+ * (Additional) effect functions (run after mount. Like with useEffect)
585
+ */
586
+ effectFns: (() => void)[] = [];
587
+
588
+ /**
589
+ * (Additional) effect cleanup functions
590
+ */
591
+ effectCleanupFns: (() => void)[] = [];
592
+
593
+ diagnosis_objectsWatchedWithOnChange = new Set<object>();
594
+
554
595
  handleRenderFinishedSuccessfully() {
555
596
  if(!this.isPassive) {
556
597
  // Delete unused loadCalls
@@ -572,6 +613,7 @@ class RenderRun {
572
613
  handleEffectSetup() {
573
614
  this.frame.persistent.hadASuccessfullMount = true;
574
615
  this.frame.startListeningForChanges();
616
+ this.effectFns.forEach(fn => fn());
575
617
  }
576
618
 
577
619
  /**
@@ -579,6 +621,7 @@ class RenderRun {
579
621
  */
580
622
  handleEffectCleanup() {
581
623
  // Call listeners:
624
+ this.effectCleanupFns.forEach(fn => fn());
582
625
  this.frame.persistent.onceOnEffectCleanupListeners.forEach(fn => fn());
583
626
  this.frame.persistent.onceOnEffectCleanupListeners = [];
584
627
 
@@ -654,7 +697,7 @@ export function watchedComponent<PROPS extends object>(componentFn:(props: PROPS
654
697
 
655
698
  renderRun.recordedReads.push(read);
656
699
  };
657
- frame.watchedProxyFacade.onAfterRead(readListener)
700
+ persistent.watchedProxyFacade.onAfterRead(readListener)
658
701
 
659
702
  try {
660
703
  try {
@@ -694,7 +737,7 @@ export function watchedComponent<PROPS extends object>(componentFn:(props: PROPS
694
737
  throw e;
695
738
  }
696
739
  finally {
697
- frame.watchedProxyFacade.offAfterRead(readListener);
740
+ persistent.watchedProxyFacade.offAfterRead(readListener);
698
741
  }
699
742
  }
700
743
  finally {
@@ -712,29 +755,61 @@ export function watchedComponent<PROPS extends object>(componentFn:(props: PROPS
712
755
 
713
756
  type WatchedOptions = {
714
757
  /**
715
- * TODO: Implement
716
758
  * Called, when a deep property was changed through the proxy.
759
+ * Setting this, will create a new proxy-facade for the purpuse of watching changes only (deep) under that proxy.
760
+ * <p>
761
+ * <a href="https://github.com/bogeeee/react-deepwatch/blob/main/readme.md#and-less-handing-onchange-listeners-to-child-components">Usage</a>
762
+ * </p>
717
763
  */
718
764
  onChange?: () => void
719
765
 
720
766
  /**
721
- * TODO: Implement
767
+ *
722
768
  * Called on a change to one of those properties, that were read-recorded in the component function (through the proxy of course).
723
769
  * Reacts also on external changes / not done through the proxy.
724
770
  */
725
- onRecordedChange?: () => void
771
+ //onRecordedChange?: () => void
726
772
  }
727
773
 
728
- function watched<T extends object>(obj: T, options?: WatchedOptions): T {
774
+ /**
775
+ * Watches any (external) object and tracks reads to it's deep childs. Changes to these tracked reads will result in a re-render or re-load the load(...) statements that depend on it.
776
+ * <p>
777
+ * Also you can use it with the onChange option, see the <a href="https://github.com/bogeeee/react-deepwatch/blob/main/readme.md#and-less-handing-onchange-listeners-to-child-components">"And less... handing onChange listeners to child components"</a> use case.
778
+ * </p>
779
+ * @param obj original object to watch
780
+ * @param options
781
+ * @returns a proxy for the original object.
782
+ */
783
+ export function watched<T extends object>(obj: T, options?: WatchedOptions): T {
729
784
  currentRenderRun || throwError("watched is not used from inside a watchedComponent");
730
- return currentRenderRun!.frame.watchedProxyFacade.getProxyFor(obj);
785
+ let result = currentRenderRun!.frame.persistent.watchedProxyFacade.getProxyFor(obj);
786
+ if(options?.onChange) {
787
+ // Safety check:
788
+ !currentRenderRun!.diagnosis_objectsWatchedWithOnChange.has(result) || throwError("You have called watched(someObject, {onChange:...}) 2 times for the same someObj. This is not supported, since for keeping as much object instance consitency as possible, there is one fixed proxyfacade-for-change-tracking assiciated to it. If you really have a valid use case for watching the same object twice for changes, submit an issue.");
789
+ currentRenderRun!.diagnosis_objectsWatchedWithOnChange.add(result)
790
+
791
+ const facadeForChangeWatching = currentRenderRun!.frame.persistent.object_to_childProxyFacade.get(obj);
792
+
793
+ // Listen for changes and call options.onChange during component mount:
794
+ const changeHandler = (changeOperation: any) => options.onChange!()
795
+ currentRenderRun!.effectFns.push(() => {facadeForChangeWatching.onAfterChange(changeHandler)});
796
+ currentRenderRun!.effectCleanupFns.push(() => {facadeForChangeWatching.offAfterChange(changeHandler)});
797
+
798
+ result = facadeForChangeWatching.getProxyFor(result);
799
+ }
800
+ return result;
731
801
  }
732
802
 
803
+ /**
804
+ * Saves the state like with useState(...) and watches and tracks reads to it's deep childs. Changes to these tracked reads will result in a re-render or re-load the load(...) statements that depend on it.
805
+ * @param initial
806
+ * @param options
807
+ */
733
808
  export function useWatchedState(initial: object, options?: WatchedOptions) {
734
809
  currentRenderRun || throwError("useWatchedState is not used from inside a watchedComponent");
735
810
 
736
811
  const [state] = useState(initial);
737
- return watched(state);
812
+ return watched(state, options);
738
813
  }
739
814
 
740
815
  /**
@@ -1080,7 +1155,7 @@ export function load(loaderFn: (oldResult?: unknown) => Promise<unknown>, option
1080
1155
  }
1081
1156
  }
1082
1157
 
1083
- function watched(value: unknown) { return (value !== null && typeof value === "object")?frame.watchedProxyFacade.getProxyFor(value):value }
1158
+ function watched(value: unknown) { return (value !== null && typeof value === "object")?persistent.watchedProxyFacade.getProxyFor(value):value }
1084
1159
 
1085
1160
  /**
1086
1161
  *
@@ -1234,4 +1309,6 @@ function probe<T>(probeFn: () => T, defaultResult: T) {
1234
1309
 
1235
1310
  export function debug_tagComponent(name: string) {
1236
1311
  currentRenderRun!.frame.persistent.debug_tag = name;
1237
- }
1312
+ }
1313
+
1314
+ export {preserve, PreserveOptions} from "./preserve"
package/index_esm.mjs CHANGED
@@ -1,6 +1,16 @@
1
1
  // A wrapper file for ESM to avoid the 'dual package hazard'. See https://nodejs.org/api/packages.html#approach-1-use-an-es-module-wrapper
2
2
 
3
3
 
4
-
5
4
  import cjsIndex from "./index.js"
6
- export const todo = cjsIndex.todo
5
+ export const watchedComponent = cjsIndex.watchedComponent
6
+ export const watched = cjsIndex.watched
7
+ export const useWatchedState = cjsIndex.useWatchedState
8
+ export const load = cjsIndex.load
9
+ export const isLoading = cjsIndex.isLoading
10
+ export const loadFailed = cjsIndex.loadFailed
11
+ export const poll = cjsIndex.poll
12
+ export const debug_tagComponent = cjsIndex.debug_tagComponent
13
+
14
+ import cjsPreserve from "./preserve.js"
15
+ export const preserve = cjsPreserve.preserve
16
+ export const PreserveOptions = cjsPreserve.PreserveOptions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-deepwatch",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "",
5
5
  "keywords": [
6
6
  "react",
@@ -44,9 +44,7 @@
44
44
  "react": "^18.3.1",
45
45
  "@types/react": "^18.3.11",
46
46
  "react-error-boundary": "^4.x",
47
- "clone": "^2.1.2",
48
- "@types/clone": "^2.1.4",
49
- "proxy-facades": "^1.0.0"
47
+ "proxy-facades": "^1.0.5"
50
48
  },
51
49
  "devDependencies": {
52
50
  "tsx": "^4.7.0",
@@ -54,6 +52,8 @@
54
52
  "rimraf": "=5.0.5",
55
53
  "ncp": "=2.0.0",
56
54
  "typescript": "^5.4.5",
57
- "vitest": "^1.5.0"
55
+ "vitest": "^1.5.0",
56
+ "clone": "^2.1.2",
57
+ "@types/clone": "^2.1.4"
58
58
  }
59
59
  }
package/readme.md CHANGED
@@ -21,13 +21,14 @@ const MyComponent = watchedComponent(props => {
21
21
  const state = useWatchedState( {myDeep: {counter: 0, b: 2}}, {/* WatchedOptions (optional) */} );
22
22
 
23
23
  return <div>
24
- Counter is: {state.myDeep.counter}
24
+ Counter is: {state.myDeep.counter}<br/>
25
25
  <button onClick={ () => state.myDeep.counter++ /* will trigger a rerender */ }>Increase counter</button>
26
26
  </div>
27
27
  }, {/* WatchedComponentOptions (optional) */});
28
28
 
29
29
  <MyComponent/> // Use MyComponent
30
30
  ````
31
+ [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz_small.svg)](https://stackblitz.com/fork/github/bogeeee/react-deepwatch/tree/1.x/examples/no-more-setstate?title=react-deepwatch%20example&file=index.tsx)
31
32
 
32
33
  ## and less... loading code
33
34
  Now that we already have the ability to deeply record our reads, let's see if there's also a way to **cut away the boilerplate code for `useEffect`**.
@@ -83,6 +84,33 @@ To reduce the number of expensive `myFetchFromServer` calls, try the following:
83
84
  # Playground [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz_small.svg)](https://stackblitz.com/fork/github/bogeeee/react-deepwatch/tree/1.x/example?title=react-deepwatch%20example&file=index.ts)
84
85
  TODO
85
86
 
87
+ # And less... handing onChange listeners to child components
88
+ Since we have proxy-facades, we can easily hook on, whenever some deep data changes and don't need to manage that with callback- passing to child-child components.
89
+ Let's say, we have a form and a child component, that modifies it. We are interested in changes, so we can send the form content to the server.
90
+ ````jsx
91
+ import {watchedComponent, watched, useWatchedState} from "react-deepwatch"
92
+ const MyParentComponent = watchedComponent(props => {
93
+ const myState = useWatchedState({
94
+ form: {
95
+ name: "",
96
+ address: ""
97
+ }
98
+ }, {onChange: () => console.log("Something deep inside myState has changed")}); // Option 1: You can hook here
99
+
100
+ return <form>
101
+ <ChildComponentThatModifiesForm form={ // let's pass a special version of myState.form to the child component which calls our onChange handler
102
+ watched(myState.form, {
103
+ onChange: () => {postFormToTheSerer(myState.form); console.log("Somthing under myState.form was changed")} // Option 2: You can also hook here
104
+ })
105
+ }/>
106
+ </form>
107
+ });
108
+ ````
109
+ _This example will trigger both onChange handlers._
110
+
111
+ _Note, that `watched(myState.form) !== myState.form`. It created a new proxy object in a new proxy-facade layer here, just for the purpose of deep-watching everything under it. Sometimes you may want to take advantage of it, so that modifications in the originaly layer (in MyParentComponent) won't fire the onChange event / call the postFormToTheSerer function. I.e. for updates that came from the server_
112
+
113
+
86
114
  # Further notes
87
115
 
88
116
  ### watched