react-jitter 0.3.1 → 0.4.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/README.md +38 -0
- package/package.json +1 -1
- package/runtime/dist/index.d.mts +3 -0
- package/runtime/dist/index.d.ts +3 -0
- package/runtime/dist/index.js +38 -12
- package/runtime/dist/index.mjs +38 -12
package/README.md
CHANGED
|
@@ -128,6 +128,44 @@ window.reactJitter.onRender = (render) => {
|
|
|
128
128
|
|
|
129
129
|
Modern bundlers will tree-shake the `import` and the function call from your production build, so it will have zero performance impact.
|
|
130
130
|
|
|
131
|
+
### Advanced: Custom Comparator Selection
|
|
132
|
+
|
|
133
|
+
By default, React Jitter uses the `deepEqual` comparator to detect changes in hook values. However, you can customize which comparator is used on a per-hook basis using the `selectComparator` function. This is useful when dealing with circular data structures or when you need different comparison strategies for different hooks.
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
// Set a custom comparator selector
|
|
137
|
+
window.reactJitter.selectComparator = (hookAddress) => {
|
|
138
|
+
// Use circularDeepEqual for hooks that might return circular structures
|
|
139
|
+
if (hookAddress.hook === 'useSelector' || hookAddress.hook === 'useReduxState') {
|
|
140
|
+
return 'circularDeepEqual';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Use deepEqual for everything else (default)
|
|
144
|
+
return 'deepEqual';
|
|
145
|
+
};
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The `hookAddress` parameter contains information about the hook:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
{
|
|
152
|
+
hook: string; // Hook name, e.g., "useState", "useContext"
|
|
153
|
+
file: string; // File path where the hook is called
|
|
154
|
+
line: number; // Line number
|
|
155
|
+
offset: number; // Column offset
|
|
156
|
+
arguments?: string[]; // Hook arguments (if includeArguments is enabled)
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Available Comparators:**
|
|
161
|
+
|
|
162
|
+
- `deepEqual` (default): Fast deep equality check that handles most cases. Will throw an error if it encounters deeply nested or circular structures.
|
|
163
|
+
- `circularDeepEqual`: Slower but handles circular references safely. Use this when your hooks return data with circular dependencies or extremely deep nesting.
|
|
164
|
+
|
|
165
|
+
**When to Use `circularDeepEqual`:**
|
|
166
|
+
|
|
167
|
+
If you see an error like "Maximum call stack size exceeded. Please use the 'circularDeepEqual' comparator", you should configure `selectComparator` to return `'circularDeepEqual'` for the specific hook mentioned in the error message.
|
|
168
|
+
|
|
131
169
|
## API and Configuration
|
|
132
170
|
|
|
133
171
|
The `reactJitter` function accepts a configuration object with two callbacks: `onHookChange` and `onRender`.
|
package/package.json
CHANGED
package/runtime/dist/index.d.mts
CHANGED
|
@@ -41,6 +41,7 @@ type HookEndEvent = {
|
|
|
41
41
|
offset: number;
|
|
42
42
|
arguments?: string[];
|
|
43
43
|
};
|
|
44
|
+
type HookAddress = Pick<HookEndEvent, 'hook' | 'file' | 'line' | 'offset' | 'arguments'>;
|
|
44
45
|
type ReactJitterOptions = {
|
|
45
46
|
enabled?: boolean;
|
|
46
47
|
onHookChange?: (change: HookChange) => void;
|
|
@@ -51,6 +52,7 @@ type ReactJitterOptions = {
|
|
|
51
52
|
}) => void;
|
|
52
53
|
};
|
|
53
54
|
type Scope = z.infer<typeof ScopeSchema>;
|
|
55
|
+
type Comparator = 'deepEqual' | 'circularDeepEqual';
|
|
54
56
|
|
|
55
57
|
type HookCall = HookChange & HookEndEvent & {
|
|
56
58
|
scope: Scope;
|
|
@@ -62,6 +64,7 @@ declare global {
|
|
|
62
64
|
reactJitter?: {
|
|
63
65
|
enabled?: boolean;
|
|
64
66
|
onHookChange?: (change: HookCall) => void;
|
|
67
|
+
selectComparator?: (hookAddress: HookAddress) => Comparator;
|
|
65
68
|
onRender?: (scope: Scope & {
|
|
66
69
|
hookResults: Record<string, unknown>;
|
|
67
70
|
renderCount: number;
|
package/runtime/dist/index.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ type HookEndEvent = {
|
|
|
41
41
|
offset: number;
|
|
42
42
|
arguments?: string[];
|
|
43
43
|
};
|
|
44
|
+
type HookAddress = Pick<HookEndEvent, 'hook' | 'file' | 'line' | 'offset' | 'arguments'>;
|
|
44
45
|
type ReactJitterOptions = {
|
|
45
46
|
enabled?: boolean;
|
|
46
47
|
onHookChange?: (change: HookChange) => void;
|
|
@@ -51,6 +52,7 @@ type ReactJitterOptions = {
|
|
|
51
52
|
}) => void;
|
|
52
53
|
};
|
|
53
54
|
type Scope = z.infer<typeof ScopeSchema>;
|
|
55
|
+
type Comparator = 'deepEqual' | 'circularDeepEqual';
|
|
54
56
|
|
|
55
57
|
type HookCall = HookChange & HookEndEvent & {
|
|
56
58
|
scope: Scope;
|
|
@@ -62,6 +64,7 @@ declare global {
|
|
|
62
64
|
reactJitter?: {
|
|
63
65
|
enabled?: boolean;
|
|
64
66
|
onHookChange?: (change: HookCall) => void;
|
|
67
|
+
selectComparator?: (hookAddress: HookAddress) => Comparator;
|
|
65
68
|
onRender?: (scope: Scope & {
|
|
66
69
|
hookResults: Record<string, unknown>;
|
|
67
70
|
renderCount: number;
|
package/runtime/dist/index.js
CHANGED
|
@@ -447,7 +447,8 @@ function createCustomEqual(options) {
|
|
|
447
447
|
}
|
|
448
448
|
|
|
449
449
|
// src/utils/getChanges.ts
|
|
450
|
-
function getChanges(prev, next) {
|
|
450
|
+
function getChanges(prev, next, comparator = "deepEqual") {
|
|
451
|
+
const equals = comparator === "circularDeepEqual" ? circularDeepEqual : deepEqual;
|
|
451
452
|
const changedKeys = [];
|
|
452
453
|
const unstableKeys = [];
|
|
453
454
|
const isObject = (v) => v !== null && typeof v === "object";
|
|
@@ -466,7 +467,7 @@ function getChanges(prev, next) {
|
|
|
466
467
|
}
|
|
467
468
|
const max = Math.max(prev.length, next.length);
|
|
468
469
|
for (let i = 0; i < max; i++) {
|
|
469
|
-
const deepEqItem =
|
|
470
|
+
const deepEqItem = equals(prev[i], next[i]);
|
|
470
471
|
const refDiffItem = isObject(prev[i]) && isObject(next[i]) && prev[i] !== next[i];
|
|
471
472
|
if (!deepEqItem || refDiffItem) {
|
|
472
473
|
const key = String(i);
|
|
@@ -479,7 +480,7 @@ function getChanges(prev, next) {
|
|
|
479
480
|
} else if (isObject(prev) && isObject(next)) {
|
|
480
481
|
const allKeys = /* @__PURE__ */ new Set([...Object.keys(prev), ...Object.keys(next)]);
|
|
481
482
|
for (const key of allKeys) {
|
|
482
|
-
const deepEqProp =
|
|
483
|
+
const deepEqProp = equals(prev[key], next[key]);
|
|
483
484
|
const refDiffProp = isObject(prev[key]) && isObject(next[key]) && prev[key] !== next[key];
|
|
484
485
|
if (!deepEqProp || refDiffProp) {
|
|
485
486
|
changedKeys.push(key);
|
|
@@ -489,7 +490,7 @@ function getChanges(prev, next) {
|
|
|
489
490
|
}
|
|
490
491
|
}
|
|
491
492
|
} else {
|
|
492
|
-
const deepEqRoot =
|
|
493
|
+
const deepEqRoot = equals(prev, next);
|
|
493
494
|
const refDiffRoot = isObject(prev) && isObject(next) && prev !== next;
|
|
494
495
|
const unstable = refDiffRoot && deepEqRoot;
|
|
495
496
|
const changed = !deepEqRoot || refDiffRoot;
|
|
@@ -500,7 +501,7 @@ function getChanges(prev, next) {
|
|
|
500
501
|
};
|
|
501
502
|
}
|
|
502
503
|
const isPlainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
503
|
-
const unstableRoot = isPlainObject(prev) && isPlainObject(next) && prev !== next &&
|
|
504
|
+
const unstableRoot = isPlainObject(prev) && isPlainObject(next) && prev !== next && equals(prev, next);
|
|
504
505
|
if (unstableRoot && changedKeys.length === 0) {
|
|
505
506
|
changedKeys.push("");
|
|
506
507
|
unstableKeys.push("");
|
|
@@ -512,6 +513,31 @@ function getChanges(prev, next) {
|
|
|
512
513
|
};
|
|
513
514
|
}
|
|
514
515
|
|
|
516
|
+
// src/utils/compareChanges.ts
|
|
517
|
+
function compareChanges(hookAddress, prev, current) {
|
|
518
|
+
var _a, _b, _c;
|
|
519
|
+
if (prev !== "undefined" && prev !== current) {
|
|
520
|
+
const comparator = (_c = (_b = (_a = window == null ? void 0 : window.reactJitter) == null ? void 0 : _a.selectComparator) == null ? void 0 : _b.call(_a, hookAddress)) != null ? _c : "deepEqual";
|
|
521
|
+
try {
|
|
522
|
+
return getChanges(prev, current, comparator);
|
|
523
|
+
} catch (error) {
|
|
524
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
525
|
+
const isRecursionError = /(?:maximum call stack(?: size)? exceeded|too much recursion|stack overflow)/i.test(
|
|
526
|
+
errorMessage
|
|
527
|
+
);
|
|
528
|
+
if (isRecursionError && comparator !== "circularDeepEqual") {
|
|
529
|
+
throw new Error(
|
|
530
|
+
`Maximum call stack size exceeded. Please use the "circularDeepEqual" comparator with selectComparator option.
|
|
531
|
+
Hook address: ${JSON.stringify(hookAddress, null, 2)}.`,
|
|
532
|
+
{ cause: error }
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
throw error;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
|
|
515
541
|
// src/index.ts
|
|
516
542
|
var scopes = {};
|
|
517
543
|
var hookStack = /* @__PURE__ */ new Map();
|
|
@@ -544,7 +570,13 @@ function useJitterScope(scope) {
|
|
|
544
570
|
const hookId = `${scopeId}-${hookEndEvent.id}`;
|
|
545
571
|
if (shouldReportChanges()) {
|
|
546
572
|
const prevResult = currentScope.hookResults[hookId];
|
|
547
|
-
const
|
|
573
|
+
const hookAddress = {
|
|
574
|
+
hook: hookEndEvent.hook,
|
|
575
|
+
file: hookEndEvent.file,
|
|
576
|
+
line: hookEndEvent.line,
|
|
577
|
+
offset: hookEndEvent.offset
|
|
578
|
+
};
|
|
579
|
+
const changes = compareChanges(hookAddress, prevResult, hookResult);
|
|
548
580
|
if (changes) {
|
|
549
581
|
const hookCall = {
|
|
550
582
|
hook: hookEndEvent.hook,
|
|
@@ -621,12 +653,6 @@ function getScopeCount(scope) {
|
|
|
621
653
|
}
|
|
622
654
|
return scopeCounter[scope.id]++;
|
|
623
655
|
}
|
|
624
|
-
function compareChanges(prev, current) {
|
|
625
|
-
if (prev !== "undefined" && prev !== current) {
|
|
626
|
-
return getChanges(prev, current);
|
|
627
|
-
}
|
|
628
|
-
return null;
|
|
629
|
-
}
|
|
630
656
|
// Annotate the CommonJS export names for ESM import in node:
|
|
631
657
|
0 && (module.exports = {
|
|
632
658
|
reactJitter,
|
package/runtime/dist/index.mjs
CHANGED
|
@@ -412,7 +412,8 @@ function createCustomEqual(options) {
|
|
|
412
412
|
}
|
|
413
413
|
|
|
414
414
|
// src/utils/getChanges.ts
|
|
415
|
-
function getChanges(prev, next) {
|
|
415
|
+
function getChanges(prev, next, comparator = "deepEqual") {
|
|
416
|
+
const equals = comparator === "circularDeepEqual" ? circularDeepEqual : deepEqual;
|
|
416
417
|
const changedKeys = [];
|
|
417
418
|
const unstableKeys = [];
|
|
418
419
|
const isObject = (v) => v !== null && typeof v === "object";
|
|
@@ -431,7 +432,7 @@ function getChanges(prev, next) {
|
|
|
431
432
|
}
|
|
432
433
|
const max = Math.max(prev.length, next.length);
|
|
433
434
|
for (let i = 0; i < max; i++) {
|
|
434
|
-
const deepEqItem =
|
|
435
|
+
const deepEqItem = equals(prev[i], next[i]);
|
|
435
436
|
const refDiffItem = isObject(prev[i]) && isObject(next[i]) && prev[i] !== next[i];
|
|
436
437
|
if (!deepEqItem || refDiffItem) {
|
|
437
438
|
const key = String(i);
|
|
@@ -444,7 +445,7 @@ function getChanges(prev, next) {
|
|
|
444
445
|
} else if (isObject(prev) && isObject(next)) {
|
|
445
446
|
const allKeys = /* @__PURE__ */ new Set([...Object.keys(prev), ...Object.keys(next)]);
|
|
446
447
|
for (const key of allKeys) {
|
|
447
|
-
const deepEqProp =
|
|
448
|
+
const deepEqProp = equals(prev[key], next[key]);
|
|
448
449
|
const refDiffProp = isObject(prev[key]) && isObject(next[key]) && prev[key] !== next[key];
|
|
449
450
|
if (!deepEqProp || refDiffProp) {
|
|
450
451
|
changedKeys.push(key);
|
|
@@ -454,7 +455,7 @@ function getChanges(prev, next) {
|
|
|
454
455
|
}
|
|
455
456
|
}
|
|
456
457
|
} else {
|
|
457
|
-
const deepEqRoot =
|
|
458
|
+
const deepEqRoot = equals(prev, next);
|
|
458
459
|
const refDiffRoot = isObject(prev) && isObject(next) && prev !== next;
|
|
459
460
|
const unstable = refDiffRoot && deepEqRoot;
|
|
460
461
|
const changed = !deepEqRoot || refDiffRoot;
|
|
@@ -465,7 +466,7 @@ function getChanges(prev, next) {
|
|
|
465
466
|
};
|
|
466
467
|
}
|
|
467
468
|
const isPlainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
468
|
-
const unstableRoot = isPlainObject(prev) && isPlainObject(next) && prev !== next &&
|
|
469
|
+
const unstableRoot = isPlainObject(prev) && isPlainObject(next) && prev !== next && equals(prev, next);
|
|
469
470
|
if (unstableRoot && changedKeys.length === 0) {
|
|
470
471
|
changedKeys.push("");
|
|
471
472
|
unstableKeys.push("");
|
|
@@ -477,6 +478,31 @@ function getChanges(prev, next) {
|
|
|
477
478
|
};
|
|
478
479
|
}
|
|
479
480
|
|
|
481
|
+
// src/utils/compareChanges.ts
|
|
482
|
+
function compareChanges(hookAddress, prev, current) {
|
|
483
|
+
var _a, _b, _c;
|
|
484
|
+
if (prev !== "undefined" && prev !== current) {
|
|
485
|
+
const comparator = (_c = (_b = (_a = window == null ? void 0 : window.reactJitter) == null ? void 0 : _a.selectComparator) == null ? void 0 : _b.call(_a, hookAddress)) != null ? _c : "deepEqual";
|
|
486
|
+
try {
|
|
487
|
+
return getChanges(prev, current, comparator);
|
|
488
|
+
} catch (error) {
|
|
489
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
490
|
+
const isRecursionError = /(?:maximum call stack(?: size)? exceeded|too much recursion|stack overflow)/i.test(
|
|
491
|
+
errorMessage
|
|
492
|
+
);
|
|
493
|
+
if (isRecursionError && comparator !== "circularDeepEqual") {
|
|
494
|
+
throw new Error(
|
|
495
|
+
`Maximum call stack size exceeded. Please use the "circularDeepEqual" comparator with selectComparator option.
|
|
496
|
+
Hook address: ${JSON.stringify(hookAddress, null, 2)}.`,
|
|
497
|
+
{ cause: error }
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
throw error;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
|
|
480
506
|
// src/index.ts
|
|
481
507
|
var scopes = {};
|
|
482
508
|
var hookStack = /* @__PURE__ */ new Map();
|
|
@@ -509,7 +535,13 @@ function useJitterScope(scope) {
|
|
|
509
535
|
const hookId = `${scopeId}-${hookEndEvent.id}`;
|
|
510
536
|
if (shouldReportChanges()) {
|
|
511
537
|
const prevResult = currentScope.hookResults[hookId];
|
|
512
|
-
const
|
|
538
|
+
const hookAddress = {
|
|
539
|
+
hook: hookEndEvent.hook,
|
|
540
|
+
file: hookEndEvent.file,
|
|
541
|
+
line: hookEndEvent.line,
|
|
542
|
+
offset: hookEndEvent.offset
|
|
543
|
+
};
|
|
544
|
+
const changes = compareChanges(hookAddress, prevResult, hookResult);
|
|
513
545
|
if (changes) {
|
|
514
546
|
const hookCall = {
|
|
515
547
|
hook: hookEndEvent.hook,
|
|
@@ -586,12 +618,6 @@ function getScopeCount(scope) {
|
|
|
586
618
|
}
|
|
587
619
|
return scopeCounter[scope.id]++;
|
|
588
620
|
}
|
|
589
|
-
function compareChanges(prev, current) {
|
|
590
|
-
if (prev !== "undefined" && prev !== current) {
|
|
591
|
-
return getChanges(prev, current);
|
|
592
|
-
}
|
|
593
|
-
return null;
|
|
594
|
-
}
|
|
595
621
|
export {
|
|
596
622
|
reactJitter,
|
|
597
623
|
useJitterScope
|