react-deepwatch 1.0.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.
package/preserve.ts ADDED
@@ -0,0 +1,492 @@
1
+ import {v} from "vitest/dist/reporters-yx5ZTtEV";
2
+ import {visitReplace} from "./Util";
3
+ import _ from "underscore";
4
+ import {invalidateObject, deleteProperty} from "proxy-facades";
5
+
6
+ const normalizeListsHint = `Hint: When this is fetched server data and having duplicate items in a list is intentional, you can pre-process it with the normalizeLists function first. See: import {normalizeLists, normalizeList} from "react-deepwatch"`
7
+
8
+ export type PreserveDiagnosis = {
9
+ fromLoad?: boolean,
10
+ callStack?: Error;
11
+ }
12
+
13
+ export type PreserveOptions = {
14
+ /**
15
+ * Invalidates those objects under newObj that have become obsolete, by installing a proxy on them that throws an a Error when trying to use them. This makes sure, your code does not accidentally use them.
16
+ * <p>Default: true</p>
17
+ */
18
+ destroyObsolete?: boolean,
19
+
20
+ /**
21
+ * Ignores the "id" property and only uses the "key" property when re-identifying objects
22
+ * <p>Default: false</p>
23
+ */
24
+ ignoresIds?: boolean,
25
+
26
+ /**
27
+ * Ignores the "key" property and only uses the "id" property when re-identifying objects
28
+ * <p>Default: false</p>
29
+ */
30
+ ignoresKeys?: boolean,
31
+
32
+ /**
33
+ * Threats an object that was preserved and reoccurs on a completely differnt place (not the same parent) as the same = re-uses that instance.
34
+ * <p>Default: false</p>
35
+ * <p>Disabled by default because too much magic / behaviour flipping depending indirectly related data.</p>
36
+ */
37
+ preserveCircular?: boolean
38
+
39
+ /**
40
+ * Only for normalizeList(s) function
41
+ */
42
+ normalize_ignoreDifferent?: boolean
43
+ }
44
+
45
+ class PreserveCall {
46
+ options: PreserveOptions;
47
+ /**
48
+ * preserved/old -> new (obsolete) object
49
+ */
50
+ mergedToNew = new Map<object,object>();
51
+
52
+ /**
53
+ * new (obsolete) object -> preserved/old object
54
+ */
55
+ newToPreserved = new Map<object,object>();
56
+
57
+ possiblyObsoleteObjects = new Set<object>();
58
+
59
+ usedObjects = new WeakSet<object>();
60
+
61
+ /**
62
+ *
63
+ * @param value
64
+ */
65
+ markUsed(value: unknown) {
66
+ if(value !== null && typeof value === "object") {
67
+ this.usedObjects.add(value);
68
+ }
69
+ }
70
+
71
+ diagnosis?: PreserveDiagnosis
72
+
73
+ constructor(options: PreserveOptions, diagnosis?: PreserveDiagnosis) {
74
+ this.options = options;
75
+ this.diagnosis = diagnosis;
76
+ }
77
+ }
78
+ type ID = string | number;
79
+
80
+ /**
81
+ * Registry for objects in the **same** Array / Set / Map
82
+ */
83
+ class ObjRegistry {
84
+ preserveOptions: PreserveOptions;
85
+
86
+ objectsById = new Map<ID, object>();
87
+ objectsByKey = new Map<ID, object>();
88
+ objectsByIdAndKey = new Map<string, object>();
89
+
90
+
91
+ constructor(preserveOptions: PreserveOptions) {
92
+ this.preserveOptions = preserveOptions;
93
+ }
94
+
95
+ getIdAndKey(obj: object, diagnosis_path: string): {id?: ID, key?: ID} {
96
+ const diagnosis_getErrorDiag = () => `Trying to reidentify the object ${diagnisis_shortenValue(obj)} during a 'preserve' operation. Path: ${diagnosis_path}`;
97
+
98
+ const result: {id?: ID, key?: ID} = {};
99
+
100
+ for(const mode of [{propName: "id", map: this.objectsById, diag_other: this.objectsByKey}, {propName: "key", map: this.objectsByKey, diag_other: this.objectsById}]) {
101
+ const throwInconsistent = (map: Map<ID, object>) => {throw new PreserveError(`Objects must be consistent in either having an id or a key or both set, but found: ${diagnisis_shortenValue(obj)} and ${diagnisis_shortenValue(map.values().next().value)}. Path: ${diagnosis_path}`)};
102
+
103
+ //@ts-ignore
104
+ const value = obj[mode.propName]
105
+ if(value === undefined || value === null) {
106
+ if(mode.map.size > 0) {
107
+ throwInconsistent(mode.map);
108
+ }
109
+ continue;
110
+ }
111
+ if(! (typeof value === "number" || typeof value === "string") ) {
112
+ throw new PreserveError(`${mode.propName} must be a number or a string. ${diagnosis_getErrorDiag()}`);
113
+ }
114
+
115
+ // safety check:
116
+ if(mode.map.size == 0 && mode.diag_other.size > 0) {
117
+ throwInconsistent(mode.diag_other);
118
+ }
119
+
120
+ //@ts-ignore
121
+ result[mode.propName] = value;
122
+ }
123
+
124
+ if(result.id === undefined && result.key === undefined) {
125
+ throw new PreserveError(`Object has no id or key set. ${diagnosis_getErrorDiag()}. Please specify 'id' or 'key' property or both on your data objects when using them in an Array/Set/Map`);
126
+ }
127
+
128
+ return result as any;
129
+ }
130
+
131
+ register(value: unknown, diagnosis_path: string) {
132
+ if (value === null || typeof value !== "object") { // not an object
133
+ return;
134
+ }
135
+
136
+ let idAndKey = this.getIdAndKey(value, diagnosis_path);
137
+ const idAndKeyString = this.idAndKeyToString(idAndKey);
138
+ const existing = this.objectsByIdAndKey.get(idAndKeyString);
139
+ if(existing !== undefined && existing !== value) {
140
+ throw new PreserveError(`Multiple items in an array have the same id+key: ${diagnisis_shortenValue(idAndKey)}. Path: ${diagnosis_path}.\n${normalizeListsHint}`)
141
+ }
142
+
143
+ // Add to maps:
144
+ this.objectsByIdAndKey.set(idAndKeyString, value);
145
+ if(idAndKey.id !== undefined) {
146
+ this.objectsById.set(idAndKey.id, value)
147
+ }
148
+ if(idAndKey.key !== undefined) {
149
+ this.objectsByKey.set(idAndKey.key, value)
150
+ }
151
+ }
152
+
153
+ get(value: object, diagnosis_path: string) {
154
+ return this.objectsByIdAndKey.get(this.idAndKeyToString(this.getIdAndKey(value, diagnosis_path)));
155
+ }
156
+
157
+ getPreserved(newValue: unknown, call: PreserveCall, diagnosis_path: string) {
158
+ if(newValue === undefined || newValue === null || typeof newValue !== "object") { // newValue is no object ?
159
+ return newValue;
160
+ }
161
+
162
+ const existing = this.get(newValue, diagnosis_path);
163
+ return preserve_inner(existing, newValue, call, diagnosis_path);
164
+ }
165
+
166
+ protected idAndKeyToString(ik: {id?: ID, key?: ID}) {
167
+ return `${ik.id}_${ik.key}`;
168
+ }
169
+ }
170
+
171
+ export function preserve<T>(oldValue: T, newValue: T, options: PreserveOptions = {}): T {
172
+ return _preserve(oldValue, newValue, options);
173
+ }
174
+
175
+ export function _preserve<T>(oldValue: T, newValue: T, options: PreserveOptions, diagnosis?: PreserveDiagnosis): T {
176
+ let call = new PreserveCall(options, diagnosis);
177
+ let result = preserve_inner(oldValue, newValue, call, "<root>");
178
+
179
+ // Invalidate obsolete objects
180
+ if(options.destroyObsolete !== false && call.possiblyObsoleteObjects.size > 0) {
181
+ const obsoleteCause = diagnosis?.callStack || new Error("Preserve was called");
182
+ call.possiblyObsoleteObjects.forEach(obj => {
183
+ if(call.usedObjects.has(obj)) {
184
+ return;
185
+ }
186
+ try {
187
+ invalidateObject(obj, "This object is obsolete. Another object is used in its place (which has all values copied to it =preserved), to keep constant object identities across data fetches. See cause. You can disable invalidation via the PreserveOptions#destroyObsolete flag.", obsoleteCause)
188
+ }
189
+ catch (e) {
190
+ throw new Error("Error during invalidation. You should disable invalidation via the PreserveOptions#destroyObsolete flag", {cause: e});
191
+ }
192
+ });
193
+ }
194
+ return result;
195
+ }
196
+
197
+
198
+ export function preserve_inner<T>(oldValue: T, newValue: T, call: PreserveCall, diagnosis_path: string): T {
199
+ const inner = () => {
200
+ if (newValue === null || typeof newValue !== "object") {
201
+ return newValue;
202
+ }
203
+
204
+ if (call.options.preserveCircular) {
205
+ const preserved = call.newToPreserved.get(newValue);
206
+ if (preserved !== undefined) {
207
+ return preserved as T;
208
+ }
209
+ }
210
+
211
+ if (!mergeable(oldValue, newValue)) {
212
+ return newValue;
213
+ }
214
+
215
+ // Narrow types:
216
+ if (oldValue === null || typeof oldValue !== "object" || newValue === null || typeof newValue !== "object") {
217
+ return newValue;
218
+ }
219
+
220
+
221
+ if (call.mergedToNew.has(oldValue)) { // Already merged or currently merging?
222
+ // Safety check:
223
+ if (call.mergedToNew.get(oldValue) !== newValue) {
224
+ throw new PreserveError(`Cannot replace object ${diagnisis_shortenValue(oldValue)} into ${diagnisis_shortenValue(newValue)} in: ${diagnosis_path}. It has already been replaced by another object: ${diagnisis_shortenValue(call.mergedToNew.get(oldValue))}. Please make your objects have a proper id or key and are not used in multiple places where these can be mistaken.\n${normalizeListsHint}`)
225
+ }
226
+
227
+ return oldValue;
228
+ }
229
+
230
+ // *** Merge: ****
231
+ call.mergedToNew.set(oldValue, newValue);
232
+ call.newToPreserved.set(newValue, oldValue);
233
+
234
+ if (Array.isArray(oldValue)) {
235
+ return preserve_array(oldValue as unknown[], newValue as unknown[], call, diagnosis_path) as T;
236
+ } else if (oldValue instanceof Set) {
237
+ return preserve_set(oldValue, newValue as any, call, diagnosis_path) as T;
238
+ } else if (oldValue instanceof Map) {
239
+ return preserve_map(oldValue, newValue as any, call, diagnosis_path) as T;
240
+ } else { // Plain objects (or class instances) ?
241
+ // ** merge objects **
242
+ // add new values:
243
+ for (const key of [...Object.getOwnPropertyNames(newValue), ...Object.getOwnPropertySymbols(newValue)]) { // iterate own keys of newValue
244
+ //@ts-ignore
245
+ oldValue[key] = preserve_inner(oldValue[key], newValue[key], call, diagnosis_path + diagnosis_jsonPath(key));
246
+ }
247
+
248
+ // remove keys not in newValue:
249
+ for (const key of [...Object.getOwnPropertyNames(oldValue), ...Object.getOwnPropertySymbols(oldValue)]) { // iterate own keys of oldValue
250
+ //@ts-ignore
251
+ if (Object.getOwnPropertyDescriptor(newValue, key) === undefined && newValue[key] === undefined) {
252
+ //@ts-ignore
253
+ deleteProperty(oldValue,key);
254
+ }
255
+ }
256
+ }
257
+ return oldValue;
258
+ }
259
+
260
+ const result = inner();
261
+ if(result !== newValue) { // old was preserved
262
+ call.possiblyObsoleteObjects.add(newValue as object);
263
+ }
264
+ else {
265
+ call.markUsed(newValue);
266
+ }
267
+
268
+ return result;
269
+ }
270
+
271
+ function preserve_array<T>(oldArray: Array<unknown>, newArray: Array<unknown>, call: PreserveCall, diagnosis_path: string): Array<unknown> {
272
+ const oldObjectRegistry = new ObjRegistry(call.options);
273
+ oldArray.forEach((v,i) => oldObjectRegistry.register(v, `${diagnosis_path}[${i}]`));
274
+
275
+ const indicesInNewArray = new Set<string>();
276
+
277
+ for(let i in newArray) {
278
+ indicesInNewArray.add(i);
279
+ const newValue = newArray[i];
280
+ oldArray[i] = oldObjectRegistry.getPreserved(newValue, call, `${diagnosis_path}[${i}]`);
281
+ }
282
+
283
+ for(let i in oldArray) {
284
+ if(!indicesInNewArray.has(i)) {
285
+ deleteProperty(oldArray,i as any); // This properly deletes the key as well, so a for...in iteration works consitent. Still it does not decrease oldArray.length
286
+ }
287
+ }
288
+
289
+ // Fix oldArray.length:
290
+ while (oldArray.length > newArray.length) {
291
+ oldArray.pop();
292
+ }
293
+
294
+ return oldArray;
295
+ }
296
+
297
+
298
+ function preserve_set<T>(oldSet: Set<unknown>, newSet: Set<unknown>, call: PreserveCall, diagnosis_path: string): Set<unknown> {
299
+
300
+ // Register old ids/keys:
301
+ const oldValuesRegistry = new ObjRegistry(call.options);
302
+ for(const value of oldSet.values()) {
303
+ oldValuesRegistry.register(value, `${diagnosis_path}`);
304
+ }
305
+
306
+ oldSet.clear();
307
+ for(const newValue of newSet.values()) {
308
+ oldSet.add(oldValuesRegistry.getPreserved(newValue, call, diagnosis_path));
309
+ }
310
+
311
+
312
+ return oldSet;
313
+ }
314
+
315
+ function preserve_map<T>(oldMap: Map<unknown, unknown>, newMap: Map<unknown, unknown>, call: PreserveCall, diagnosis_path: string): Map<unknown, unknown> {
316
+
317
+ // Register old ids/keys:
318
+ const oldKeysRegistry = new ObjRegistry(call.options);
319
+ for(const key of oldMap.keys()) {
320
+ oldKeysRegistry.register(key, `${diagnosis_path}`);
321
+ }
322
+ const oldValuesRegistry = new ObjRegistry(call.options);
323
+ for(const value of oldMap.values()) {
324
+ oldValuesRegistry.register(value, `${diagnosis_path}`);
325
+ }
326
+
327
+ oldMap.clear();
328
+ for(let newKey of newMap.keys()) {
329
+ let newValue = newMap.get(newKey);
330
+ oldMap.set(oldKeysRegistry.getPreserved(newKey, call, diagnosis_path), oldValuesRegistry.getPreserved(newValue, call, `${diagnosis_path}[${diagnisis_shortenValue(newKey)}]`));
331
+ }
332
+
333
+ return oldMap;
334
+ }
335
+
336
+
337
+ function isSameObjectType(a: object, b: object) {
338
+ return Object.getPrototypeOf(a) === Object.getPrototypeOf(b) || a.constructor === b.constructor;
339
+ }
340
+
341
+ function diagnosis_jsonPath(key: unknown) {
342
+ if(!Number.isNaN(Number(key))) {
343
+ return `[${key}]`;
344
+ }
345
+ return `.${key}`;
346
+ }
347
+
348
+ class PreserveError extends Error {
349
+
350
+ }
351
+
352
+ export function diagnisis_shortenValue(evil_value: any) : string {
353
+ if(evil_value === undefined) {
354
+ return "undefined";
355
+ }
356
+
357
+ if(evil_value === null) {
358
+ return "null";
359
+ }
360
+
361
+ let objPrefix = "";
362
+ if(typeof evil_value == "object" && evil_value.constructor?.name && evil_value.constructor?.name !== "Object") {
363
+ objPrefix = `class ${evil_value.constructor?.name} `;
364
+ }
365
+
366
+
367
+
368
+ function shorten(value: string) {
369
+ const MAX = 50;
370
+ if (value.length > MAX) {
371
+ return value.substring(0, MAX) + "..."
372
+ }
373
+ return value;
374
+ }
375
+
376
+ try {
377
+ return shorten(objPrefix + betterJsonStringify(evil_value));
378
+ }
379
+ catch (e) {
380
+ }
381
+
382
+ if(typeof evil_value == "string") {
383
+ return shorten(evil_value)
384
+ }
385
+ else if(typeof evil_value == "object") {
386
+ return `${objPrefix}{...}`;
387
+ }
388
+ else {
389
+ return "unknown"
390
+ }
391
+
392
+ /**
393
+ * Like JSON.stringify, but support for some additional types.
394
+ *
395
+ * @param value
396
+ */
397
+ function betterJsonStringify(value: unknown) {
398
+ return JSON.stringify(value,(key, val) => {
399
+ if(val === undefined){
400
+ return "undefined"
401
+ }
402
+ else if(typeof val === 'number' && isNaN(val)){
403
+ return "NaN";
404
+ }
405
+ else if(val !== null && JSON.stringify(val) === "null") {
406
+ return "-unknown type-";
407
+ }
408
+ else if(val instanceof Set) {
409
+ return "-Set(...)-";
410
+ }
411
+ else if(val instanceof Map) {
412
+ return "-Map(...)-";
413
+ }
414
+ else if(val instanceof RegExp) {
415
+ return "-Regexp(...)-";
416
+ }
417
+ return val;
418
+ });
419
+ }
420
+
421
+ }
422
+
423
+ function mergeable(oldValue: unknown, newValue: unknown) {
424
+ // Return if both are not compatible objects:
425
+ if(oldValue === undefined || oldValue === null || typeof oldValue !== "object") { // oldValue is no object ?
426
+ return false;
427
+ }
428
+ if(newValue === undefined || newValue === null || typeof newValue !== "object") { // new is no object ?
429
+ return false;
430
+ }
431
+ if(!isSameObjectType(oldValue, newValue)) {
432
+ return false;
433
+ }
434
+
435
+ if(oldValue === newValue) {
436
+ return false; // nothing to do
437
+ }
438
+
439
+ if(newValue instanceof WeakSet) {
440
+ return false; // Merging unsupported
441
+ }
442
+ else if(newValue instanceof WeakMap) {
443
+ return false; // Merging unsupported
444
+ }
445
+
446
+
447
+ return true;
448
+ }
449
+
450
+ /**
451
+ *
452
+ * Scans root deeply for Arrays and for each found array, it call normalizeList, which squeezes the items with the same id/key into one object instance.
453
+ * @see normalizeList
454
+ * @param root
455
+ * @param options
456
+ */
457
+ export function normalizeLists<T extends object>(root: T, options: PreserveOptions = {}) {
458
+ return visitReplace(root, (value, visitChilds, context) => {
459
+ if(Array.isArray(value)) {
460
+ normalizeList(value, options, context.diagnosis_path);
461
+ }
462
+ return visitChilds(value, context);
463
+ }, "onError");
464
+ }
465
+
466
+ /**
467
+ * Squeezes the items with the same id/key into one object instance.
468
+ * @param list
469
+ * @param options
470
+ * @param diagnosis_path internal
471
+ */
472
+ export function normalizeList<T extends unknown[]>(list: T, options: PreserveOptions = {}, diagnosis_path?: string): T {
473
+ const objRegistry = new ObjRegistry(options);
474
+ for(let i in list) {
475
+ const value = list[i];
476
+ if(value === null || typeof value !== "object") { // value is no object ?
477
+ continue;
478
+ }
479
+ let existing = objRegistry.get(value, diagnosis_path || "<list>");
480
+ if(existing !== undefined) {
481
+ // Safety check:
482
+ if(!options.normalize_ignoreDifferent && !_.isEqual(existing, value)) {
483
+ throw new Error(`Array-items at indexes ${list.findIndex(v => v === existing)} and ${i} have the same id/key but different content: ${diagnisis_shortenValue(existing)} vs. ${diagnisis_shortenValue(value)} Path: ${diagnosis_path}. You can set the normalize_ignoreDifferent option to ignore this.`)
484
+ }
485
+ //@ts-ignore
486
+ list[i] = existing;
487
+ continue;
488
+ }
489
+ objRegistry.register(value, diagnosis_path || "<list>");
490
+ }
491
+ return list;
492
+ }
package/readme.md ADDED
@@ -0,0 +1,109 @@
1
+ # React Deepwatch - no more setState and less
2
+
3
+
4
+ **Deeply watches your state-object and props** for changes. **Re-renders** automatically😎 and makes you write less code 😊.
5
+ - **Performance friendly**
6
+ React Deepwatch uses [proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) facades to **watch only for those properties that are actually used** in your component function. It doesn't matter how complex and deep the graph behind your state or props is.
7
+ - **Can watch your -model- as well**
8
+ If a (used) property in props points to your model, a change there will also trigger a re-render. In fact, you can [watch anything](#watched) ;)
9
+
10
+ # Install
11
+ ````bash
12
+ npm install --save react-deepwatch
13
+ ````
14
+
15
+ # Usage
16
+ ## no more setState
17
+ ````jsx
18
+ import {watchedComponent, watched, useWatchedState} from "react-deepwatch"
19
+
20
+ const MyComponent = watchedComponent(props => {
21
+ const state = useWatchedState( {myDeep: {counter: 0, b: 2}}, {/* WatchedOptions (optional) */} );
22
+
23
+ return <div>
24
+ Counter is: {state.myDeep.counter}
25
+ <button onClick={ () => state.myDeep.counter++ /* will trigger a rerender */ }>Increase counter</button>
26
+ </div>
27
+ }, {/* WatchedComponentOptions (optional) */});
28
+
29
+ <MyComponent/> // Use MyComponent
30
+ ````
31
+
32
+ ## and less... loading code
33
+ 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`**.
34
+
35
+ ````jsx
36
+ import {watchedComponent, load, poll, isLoading, loadFailed, preserve} from "react-deepwatch"
37
+
38
+ const MyComponent = watchedComponent(props => {
39
+
40
+ return <div>
41
+ Here's something fetched from the Server: { load( async () => await myFetchFromServer(props.myProperty), {/* LoadOptions (optional) */} ) }
42
+ </div>
43
+ });
44
+
45
+ <MyComponent/> // Use MyComponent
46
+ ````
47
+ **`load(...)` re-executes `myFetchFromServer`, when a dependent value changes**. That means, it records all reads from previous code in your component function plus the reads immediately inside the `load(...)` call. _Here: props.myProperty._
48
+ The returned Promise will be await'ed and the component will be put into [suspense](https://react.dev/reference/react/Suspense) that long.
49
+ 👍 load(...) can be inside a conditional block or a loop. Then it has already recorded the condition + everything else that leads to the computation of load(...)'s point in time and state 😎._
50
+ For this mechanic to work, **make sure, all sources are watched**: `props` and `load(...)`'s result are already automatically watched; For state, use `useWatchedState(...)`; For context, use `watched(useContext(...))`.
51
+
52
+ ### Show a 🌀loading spinner
53
+ To show a 🌀loading spinner / placeholder during load, either...
54
+ - **wrap your component in a [`<Suspense fallback={<div>🌀</div>}>...<MyComponent/>...</Suspense>`](https://react.dev/reference/react/Suspense)**. It can be wrapped at any parent level😎. _Or..._
55
+ - **call isLoading()** inside your component, to probe if any or a certain `load(...)`statement is loading. _See jsDoc for usage example. Mind the caveat of not using it for a condition to cut off a load statement._ _and/or..._
56
+ - **specify a fallback** value via `load(..., {fallback:"🌀"})`.
57
+
58
+ ### Handle errors
59
+ either...
60
+ - **wrap your component in a** [`<ErrorBoundary fallback={<div>Something went wrong</div>}>...<MyComponent/>...</ErrorBoundary>`](https://github.com/bvaughn/react-error-boundary) from the [react-error-boundary](https://github.com/bvaughn/react-error-boundary) package. It can be wrapped at any parent level😎.
61
+ It tries to recover from errors and re- runs the `loaderFn`, whenever a dependency changes. Note that recovering works only with the mentioned [react-error-boundary 4.x](https://github.com/bvaughn/react-error-boundary) and not with 3rd party error-boundary libraries. _Or..._
62
+ - **try/catch around the load(...)** statement. Caveat: You must check, if caught is `instanceof Promise` and re-throw it then. _Because this is the way for `load` to signal, that things are loading._ _Or..._
63
+ - **call** the **loadFailed()** probing function. This looks more elegant than the above. _See jsDoc for usage example._
64
+
65
+ ### Performance optimization for load(...)
66
+ To reduce the number of expensive `myFetchFromServer` calls, try the following:
67
+ - Move the load(...) call as upwards in the code as possible, so it depends on fewer props / state / watched objects.
68
+ - See the `LoadOptions#fallback`, `LoadOptions#silent` and `LoadOptions#critical` settings.
69
+ - Use the `preserve` function on all your fetched data, to smartly ensure non-changing object instances in your app (`newFetchResult` **===** `oldFetchResult`; Triple-equals. Also for the deep result_). Changed object instances can either cascade to a lot of re-loads or result in your component still watching the old instance.
70
+ _Think of it like: The preserve function does for your data, what React does for your component tree: It smartly remembers the instances, if needed with the help of an id or key, and re-applies the re-fetched/re-rendered properties to them, so the object-identity/component-state stays the same._
71
+ 👍 `load(...)` does call `preserve` by default to enforce this paradigm and give you the best, trouble free experience.
72
+
73
+ ### Caveats
74
+ - The component function might return and empty `</>` on the first load and **produce a short screen flicker**. This is [because React's Suspense mechasim is not able to remeber state at that time](https://react.dev/reference/react/Suspense#caveats). To circumvent this, specify `WatchedComponentOptions#fallback`.
75
+ - `<Suspense>` and `<ErrorBoundary>` inside your component function do not handle/catch loads in that **same** function. _Means: You must place them outside to handle/catch them._
76
+ - If your app is a mixed scenario with non-watchedComponents and relies on the old way of fully re-rendering the whole tree to pass deep model data (=more than using shallow, primitive props) to the leaves, mind disabling the WatchedComponentOptions#memo flag.
77
+ - SSR is not supported.
78
+ - [startTransition](https://react.dev/reference/react/startTransition) is not supported (has no effect).
79
+ - As said: Keep in mind that `load(...)` calls `preserve` on its result. It also invalidates (destroys) the "unused" objects. _When they're not really unused any you are trying to access them, You'll get the proper error message how to disable it_.
80
+
81
+ # [Deeper explaining the mechanics](mechanics.md)
82
+
83
+ # 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
+ TODO
85
+
86
+ # Further notes
87
+
88
+ ### watched
89
+ You can also use `watched` similarly to `useWatchedState` to watch any global object. _But in React paradigm, this is rather rare, because values are usually passed as props into your component function._
90
+
91
+ ### poll
92
+ Besides `load`, there's also the `poll` function, which works similar, but re-loads in regular intervals. _See jsDoc_
93
+
94
+ ### Simplify the server side as well
95
+ If you like, how this library simplifies things for you and want to write the backend (http) endpoints behind your load(...) statements simply as typescript methods, have a look at my flagship project [Restfuncs](https://github.com/bogeeee/restfuncs).
96
+ Example:
97
+ ````typescript
98
+ // In your watchedComponent function:
99
+ return <div>The greeting's result from server is: { load( async () => await myRemoteSession.greet(state.name) ) }</div>
100
+
101
+ // On the server:
102
+ ...
103
+ @remote greet(name: string) {
104
+ return `Hello ${name}`
105
+ }
106
+ ...
107
+ ````
108
+ _The example leaves away all the the setup-once boilerplate code.
109
+ Also in your tsx, you can enjoy type awareness / type safety and IDE's code completion around `myRemoteSession.greet` and all its parameters and returned types, which is a feature that only rpc libraries can offer (Restfuncs is such one)😎_