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/Util.d.ts +13 -0
- package/Util.d.ts.map +1 -1
- package/Util.js +31 -4
- package/Util.js.map +1 -1
- package/Util.ts +32 -3
- package/dist/mjs/Util.d.ts +13 -0
- package/dist/mjs/Util.d.ts.map +1 -1
- package/dist/mjs/Util.js +28 -3
- package/dist/mjs/Util.js.map +1 -1
- package/dist/mjs/index.d.ts +30 -11
- package/dist/mjs/index.d.ts.map +1 -1
- package/dist/mjs/index.js +65 -21
- package/dist/mjs/index.js.map +1 -1
- package/index.d.ts +30 -11
- package/index.d.ts.map +1 -1
- package/index.js +66 -19
- package/index.js.map +1 -1
- package/index.ts +103 -26
- package/index_esm.mjs +12 -2
- package/package.json +5 -5
- package/readme.md +29 -1
package/index.ts
CHANGED
|
@@ -4,15 +4,19 @@ import {
|
|
|
4
4
|
RecordedValueRead,
|
|
5
5
|
WatchedProxyFacade, installChangeTracker
|
|
6
6
|
} from "proxy-facades";
|
|
7
|
-
import {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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")?
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
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
|
+
[](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 [](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
|