rerender-lens 0.4.0 → 0.5.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/CHANGELOG.md +78 -0
- package/README.md +192 -362
- package/dist/{budget-COBu7jBU.d.cts → budget-BnJlYwrV.d.ts} +20 -10
- package/dist/{budget-LkNjGtRc.d.ts → budget-CRswn-nC.d.cts} +20 -10
- package/dist/cli.cjs +248 -31
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +248 -31
- package/dist/cli.js.map +1 -1
- package/dist/{devtools-CZebzpF6.d.ts → devtools-BYWat7nQ.d.ts} +7 -1
- package/dist/{devtools-BkCct3cJ.d.cts → devtools-DJi_6AfJ.d.cts} +7 -1
- package/dist/index.cjs +439 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +131 -8
- package/dist/index.d.ts +131 -8
- package/dist/index.js +424 -42
- package/dist/index.js.map +1 -1
- package/dist/jest-setup.cjs +1673 -0
- package/dist/jest-setup.cjs.map +1 -0
- package/dist/jest-setup.d.cts +8 -0
- package/dist/jest-setup.d.ts +8 -0
- package/dist/jest-setup.js +1671 -0
- package/dist/jest-setup.js.map +1 -0
- package/dist/jest.cjs +1760 -0
- package/dist/jest.cjs.map +1 -0
- package/dist/jest.d.cts +23 -0
- package/dist/jest.d.ts +23 -0
- package/dist/jest.js +1751 -0
- package/dist/jest.js.map +1 -0
- package/dist/{notifiers-BjjGSHpp.d.ts → notifiers-DwKlvv4H.d.cts} +8 -4
- package/dist/{notifiers-BGQtWKfX.d.cts → notifiers-FNzKHs0a.d.ts} +8 -4
- package/dist/playwright.cjs +230 -28
- package/dist/playwright.cjs.map +1 -1
- package/dist/playwright.d.cts +4 -4
- package/dist/playwright.d.ts +4 -4
- package/dist/playwright.js +230 -28
- package/dist/playwright.js.map +1 -1
- package/dist/relay.cjs +10 -2
- package/dist/relay.cjs.map +1 -1
- package/dist/relay.js +10 -2
- package/dist/relay.js.map +1 -1
- package/dist/rerender-lens.iife.js +356 -54
- package/dist/runner-report-B8Sc3FZO.d.cts +46 -0
- package/dist/runner-report-CW8X5RwK.d.ts +46 -0
- package/dist/setup.cjs +112 -27
- package/dist/setup.cjs.map +1 -1
- package/dist/setup.js +112 -27
- package/dist/setup.js.map +1 -1
- package/dist/{types-BzUEVkxJ.d.cts → types-DW5-N2XH.d.cts} +55 -2
- package/dist/{types-BzUEVkxJ.d.ts → types-DW5-N2XH.d.ts} +55 -2
- package/dist/vite.d.cts +1 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vitest-setup.cjs +337 -41
- package/dist/vitest-setup.cjs.map +1 -1
- package/dist/vitest-setup.d.cts +4 -4
- package/dist/vitest-setup.d.ts +4 -4
- package/dist/vitest-setup.js +337 -41
- package/dist/vitest-setup.js.map +1 -1
- package/dist/vitest.cjs +400 -77
- package/dist/vitest.cjs.map +1 -1
- package/dist/vitest.d.cts +21 -39
- package/dist/vitest.d.ts +21 -39
- package/dist/vitest.js +401 -78
- package/dist/vitest.js.map +1 -1
- package/package.json +40 -4
- package/panel/panel.css +1 -0
- package/panel/panel.js +499 -309
package/dist/vitest-setup.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { LensSetup } from './
|
|
2
|
-
import './types-
|
|
3
|
-
import './notifiers-
|
|
4
|
-
import './budget-
|
|
1
|
+
import { L as LensSetup } from './runner-report-B8Sc3FZO.cjs';
|
|
2
|
+
import './types-DW5-N2XH.cjs';
|
|
3
|
+
import './notifiers-DwKlvv4H.cjs';
|
|
4
|
+
import './budget-CRswn-nC.cjs';
|
|
5
5
|
|
|
6
6
|
declare const lens: LensSetup;
|
|
7
7
|
|
package/dist/vitest-setup.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { LensSetup } from './
|
|
2
|
-
import './types-
|
|
3
|
-
import './notifiers-
|
|
4
|
-
import './budget-
|
|
1
|
+
import { L as LensSetup } from './runner-report-CW8X5RwK.js';
|
|
2
|
+
import './types-DW5-N2XH.js';
|
|
3
|
+
import './notifiers-FNzKHs0a.js';
|
|
4
|
+
import './budget-BnJlYwrV.js';
|
|
5
5
|
|
|
6
6
|
declare const lens: LensSetup;
|
|
7
7
|
|
package/dist/vitest-setup.js
CHANGED
|
@@ -127,6 +127,7 @@ function deepEqualObjects(a, b, seen) {
|
|
|
127
127
|
}
|
|
128
128
|
return false;
|
|
129
129
|
}
|
|
130
|
+
var AVOIDABLE_KINDS = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
|
|
130
131
|
function classify(prev, next) {
|
|
131
132
|
if (typeof prev === "function" && typeof next === "function") {
|
|
132
133
|
return prev.name === next.name && prev.toString() === next.toString() ? "function" : "different";
|
|
@@ -163,30 +164,30 @@ function diffRecords(prev, next, basePath = "") {
|
|
|
163
164
|
if (Object.is(a, b)) continue;
|
|
164
165
|
const kind = classify(a, b);
|
|
165
166
|
if (kind === "different") {
|
|
166
|
-
changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path), kind, prev: a, next: b });
|
|
167
|
+
changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path) ?? path, kind, prev: a, next: b });
|
|
167
168
|
} else {
|
|
168
169
|
changes.push({ path, kind, prev: a, next: b });
|
|
169
170
|
}
|
|
170
171
|
}
|
|
171
172
|
return changes;
|
|
172
173
|
}
|
|
173
|
-
function firstDifferentPath(a, b, path, depth = 0) {
|
|
174
|
-
if (depth > 8) return path;
|
|
174
|
+
function firstDifferentPath(a, b, path = "", depth = 0) {
|
|
175
|
+
if (depth > 8) return path || "(value)";
|
|
175
176
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
176
|
-
if (a.length !== b.length) return `${path}.length
|
|
177
|
+
if (a.length !== b.length) return path ? `${path}.length` : "length";
|
|
177
178
|
for (let i = 0; i < a.length; i++) {
|
|
178
179
|
if (!deepEqual(a[i], b[i])) return firstDifferentPath(a[i], b[i], joinPath(path, i), depth + 1);
|
|
179
180
|
}
|
|
180
|
-
return
|
|
181
|
+
return null;
|
|
181
182
|
}
|
|
182
183
|
if (isPlainObject(a) && isPlainObject(b)) {
|
|
183
184
|
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
184
185
|
for (const k of keys) {
|
|
185
186
|
if (!deepEqual(a[k], b[k])) return firstDifferentPath(a[k], b[k], joinPath(path, k), depth + 1);
|
|
186
187
|
}
|
|
187
|
-
return
|
|
188
|
+
return null;
|
|
188
189
|
}
|
|
189
|
-
return path;
|
|
190
|
+
return deepEqual(a, b) ? null : path || "(value)";
|
|
190
191
|
}
|
|
191
192
|
|
|
192
193
|
// src/report.ts
|
|
@@ -204,10 +205,12 @@ function storeAdvice(custom) {
|
|
|
204
205
|
}
|
|
205
206
|
var isGenuine = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
|
|
206
207
|
var isChildren = (change) => change.path === "children";
|
|
208
|
+
var isRefObject = (change) => change.path === "ref" && change.kind === "deep-equal";
|
|
207
209
|
function fixFor(change) {
|
|
208
210
|
if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
|
|
209
211
|
return `lift the children out of the parent's render: memoize them with useMemo, hoist static elements to module scope, or render them from a component that does not re-render`;
|
|
210
212
|
}
|
|
213
|
+
if (isRefObject(change)) return `create the ref once with useRef (or createRef outside the render) and pass the same object every time`;
|
|
211
214
|
switch (change.kind) {
|
|
212
215
|
case "function":
|
|
213
216
|
return `wrap it in useCallback (or hoist it out of the parent's render)`;
|
|
@@ -223,6 +226,7 @@ function describe(change) {
|
|
|
223
226
|
if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
|
|
224
227
|
return `children are new React elements with the same types and props on every render of the parent`;
|
|
225
228
|
}
|
|
229
|
+
if (isRefObject(change)) return `prop "ref" is a new ref object on every render (React re-attaches it each time)`;
|
|
226
230
|
switch (change.kind) {
|
|
227
231
|
case "deep-equal":
|
|
228
232
|
return `prop "${change.path}" is a new reference but deep-equal to the previous value`;
|
|
@@ -250,11 +254,21 @@ function buildReport(input) {
|
|
|
250
254
|
if (causes.length === 0) trigger = "parent";
|
|
251
255
|
else if (causes.length === 1) trigger = causes[0];
|
|
252
256
|
else trigger = "mixed";
|
|
253
|
-
const
|
|
257
|
+
const nothingChanged = input.propChanges.length === 0 && stateChanges.length === 0 && hookChanges.length === 0;
|
|
258
|
+
const cacheHit = input.compiled === true && trigger === "parent" && input.propChanges.length === 0;
|
|
259
|
+
const revealed = input.revealed === true && trigger === "parent";
|
|
260
|
+
const avoidable = trigger === "parent" && !cacheHit && !revealed;
|
|
254
261
|
const reasons = [];
|
|
255
|
-
|
|
262
|
+
const memoFix = input.classComponent ? `Make "${input.component}" extend PureComponent (or implement shouldComponentUpdate)` : `Wrap "${input.component}" in React.memo (or extend PureComponent)`;
|
|
263
|
+
if (cacheHit) {
|
|
264
|
+
reasons.push(`compiled by React Compiler: its output is memoized and the inputs did not change, so this render is cheap; nothing to fix.`);
|
|
265
|
+
} else if (revealed) {
|
|
266
|
+
reasons.push(
|
|
267
|
+
`re-rendered because the Suspense boundary above it switched from its fallback to content: React re-renders the content it kept hidden while the fallback was shown. A one-off; nothing to fix.`
|
|
268
|
+
);
|
|
269
|
+
} else if (avoidable && nothingChanged) {
|
|
256
270
|
const who = input.parent ? `<${input.parent.name}> re-rendered (${describeTrigger(input.parent.trigger)})` : "its parent re-rendered";
|
|
257
|
-
reasons.push(`re-rendered with identical props because ${who}.
|
|
271
|
+
reasons.push(`re-rendered with identical props because ${who}. ${memoFix}.`);
|
|
258
272
|
} else if (avoidable && input.parent) {
|
|
259
273
|
reasons.push(`caused by <${input.parent.name}> re-rendering (${describeTrigger(input.parent.trigger)}).`);
|
|
260
274
|
}
|
|
@@ -263,9 +277,9 @@ function buildReport(input) {
|
|
|
263
277
|
reasons.push(fix ? `${describe(c)}: ${fix}.` : `${describe(c)}.`);
|
|
264
278
|
}
|
|
265
279
|
const memoized = input.memoized !== false;
|
|
266
|
-
if (avoidable && !memoized && input.propChanges.length > 0) {
|
|
280
|
+
if (avoidable && !memoized && !input.compiled && input.propChanges.length > 0) {
|
|
267
281
|
reasons.push(
|
|
268
|
-
`"${input.component}" is not memoized, so fixing the props alone will not stop this re-render: also wrap it in React.memo (or extend PureComponent).`
|
|
282
|
+
`"${input.component}" is not memoized, so fixing the props alone will not stop this re-render: also ${input.classComponent ? "extend PureComponent (or implement shouldComponentUpdate)" : "wrap it in React.memo (or extend PureComponent)"}.`
|
|
269
283
|
);
|
|
270
284
|
}
|
|
271
285
|
for (const c of stateChanges) {
|
|
@@ -277,6 +291,8 @@ function buildReport(input) {
|
|
|
277
291
|
const where = c.provider && c.provider.component ? ` (provided by <${c.provider.component}>)` : "";
|
|
278
292
|
const keys = c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys ? `: only ${c.changedKeys.map((k) => `"${k}"`).join(", ")} of ${c.totalKeys} keys changed, yet every consumer re-renders. Split the context or memoize the slices consumers read` : "";
|
|
279
293
|
reasons.push(`${c.path} changed${where}${keys}.`);
|
|
294
|
+
} else if (c.hook === "useDeferredValue" && isGenuine(c)) {
|
|
295
|
+
reasons.push(`${hookLabel(c)} caught up with its latest value: this is the deferred second render React schedules after the urgent one; nothing to fix.`);
|
|
280
296
|
} else if (isGenuine(c)) reasons.push(`${hookLabel(c)} changed.`);
|
|
281
297
|
else if (isStateHook(c))
|
|
282
298
|
reasons.push(`${hookLabel(c)} was set to a value deep-equal to the current one (new reference, same contents): reuse the existing object or bail out before calling the setter.`);
|
|
@@ -307,6 +323,7 @@ function buildReport(input) {
|
|
|
307
323
|
reasons,
|
|
308
324
|
time: now()
|
|
309
325
|
};
|
|
326
|
+
if (input.compiled) report.compiled = true;
|
|
310
327
|
if (input.selfDuration !== void 0) report.selfDuration = input.selfDuration;
|
|
311
328
|
if (input.treeDuration !== void 0) report.treeDuration = input.treeDuration;
|
|
312
329
|
if (input.commitPriority) report.commitPriority = input.commitPriority;
|
|
@@ -344,10 +361,10 @@ var KIND_LABEL = {
|
|
|
344
361
|
};
|
|
345
362
|
function summarize(report) {
|
|
346
363
|
const counts2 = /* @__PURE__ */ new Map();
|
|
347
|
-
for (const c of [...report.propChanges, ...report.stateChanges, ...report.hookChanges]) {
|
|
364
|
+
for (const c of [...report.propChanges || [], ...report.stateChanges || [], ...report.hookChanges || []]) {
|
|
348
365
|
counts2.set(c.kind, (counts2.get(c.kind) ?? 0) + 1);
|
|
349
366
|
}
|
|
350
|
-
const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind]}`);
|
|
367
|
+
const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind] || kind}`);
|
|
351
368
|
if (parts.length === 0) parts.push("no changes");
|
|
352
369
|
return parts.join(", ");
|
|
353
370
|
}
|
|
@@ -471,6 +488,27 @@ function disable() {
|
|
|
471
488
|
}
|
|
472
489
|
|
|
473
490
|
// src/hookNames.ts
|
|
491
|
+
var NODE_COUNT = {
|
|
492
|
+
useState: 1,
|
|
493
|
+
useReducer: 1,
|
|
494
|
+
useRef: 1,
|
|
495
|
+
useMemo: 1,
|
|
496
|
+
useCallback: 1,
|
|
497
|
+
useEffect: 1,
|
|
498
|
+
useLayoutEffect: 1,
|
|
499
|
+
useInsertionEffect: 1,
|
|
500
|
+
useImperativeHandle: 1,
|
|
501
|
+
useSyncExternalStore: 2,
|
|
502
|
+
useTransition: 2,
|
|
503
|
+
useDeferredValue: 1,
|
|
504
|
+
useId: 1,
|
|
505
|
+
useOptimistic: 1,
|
|
506
|
+
useActionState: 3,
|
|
507
|
+
useFormState: 3,
|
|
508
|
+
useEffectEvent: 1,
|
|
509
|
+
useCacheRefresh: 1
|
|
510
|
+
};
|
|
511
|
+
var BUILTIN_HOOKS = /* @__PURE__ */ new Set([...Object.keys(NODE_COUNT), "useContext", "useDebugValue", "use"]);
|
|
474
512
|
var noop = () => {
|
|
475
513
|
};
|
|
476
514
|
function customHooksFromStack(stack, componentName, marker) {
|
|
@@ -485,7 +523,7 @@ function customHooksFromStack(stack, componentName, marker) {
|
|
|
485
523
|
continue;
|
|
486
524
|
}
|
|
487
525
|
if (name === componentName || name.endsWith("." + componentName)) break;
|
|
488
|
-
if (/^use[A-Z0-9_]/.test(name)) names.push(name);
|
|
526
|
+
if (/^use[A-Z0-9_]/.test(name) && !BUILTIN_HOOKS.has(name)) names.push(name);
|
|
489
527
|
if (names.length > 8) break;
|
|
490
528
|
}
|
|
491
529
|
return names;
|
|
@@ -769,6 +807,11 @@ function isMemoizedFiber(fiber) {
|
|
|
769
807
|
}
|
|
770
808
|
return false;
|
|
771
809
|
}
|
|
810
|
+
function isCompiledFiber(fiber) {
|
|
811
|
+
if (fiber.tag === ClassComponent) return false;
|
|
812
|
+
const cache2 = fiber.updateQueue?.memoCache;
|
|
813
|
+
return !!cache2 && Array.isArray(cache2.data);
|
|
814
|
+
}
|
|
772
815
|
function durationsOf(fiber) {
|
|
773
816
|
if (typeof fiber.actualDuration !== "number") return null;
|
|
774
817
|
const tree = fiber.actualDuration;
|
|
@@ -921,7 +964,8 @@ function diffHooks(fiber, alt) {
|
|
|
921
964
|
const labels = hookLabelsFor(fiber, b);
|
|
922
965
|
let i = 0;
|
|
923
966
|
while (a && b) {
|
|
924
|
-
|
|
967
|
+
const deferred = labels?.[i] === "useDeferredValue";
|
|
968
|
+
if ((deferred || isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
|
|
925
969
|
const hook = labels?.[i] ?? hookLabel2(b);
|
|
926
970
|
out.push({
|
|
927
971
|
path: `${hook}#${i}`,
|
|
@@ -955,6 +999,10 @@ function diffContexts(fiber, alt) {
|
|
|
955
999
|
let a = alt.dependencies?.firstContext ?? null;
|
|
956
1000
|
let b = fiber.dependencies?.firstContext ?? null;
|
|
957
1001
|
let i = 0;
|
|
1002
|
+
if (b && !("memoizedValue" in b)) {
|
|
1003
|
+
warnOnce("contextValues", "this React does not record context values on the fiber (React < 18): context changes are reported as plain parent re-renders.");
|
|
1004
|
+
return out;
|
|
1005
|
+
}
|
|
958
1006
|
while (a && b) {
|
|
959
1007
|
if (a.context === b.context && !Object.is(a.memoizedValue, b.memoizedValue)) {
|
|
960
1008
|
const name = b.context.displayName ?? "Context";
|
|
@@ -987,10 +1035,26 @@ function diffContexts(fiber, alt) {
|
|
|
987
1035
|
return out;
|
|
988
1036
|
}
|
|
989
1037
|
var isGenuine2 = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
|
|
1038
|
+
var isRefObject2 = (v) => {
|
|
1039
|
+
if (typeof v !== "object" || v === null) return false;
|
|
1040
|
+
const keys = Object.keys(v);
|
|
1041
|
+
return keys.length === 1 && keys[0] === "current";
|
|
1042
|
+
};
|
|
1043
|
+
function diffProps(prev, next) {
|
|
1044
|
+
if (prev === next) return [];
|
|
1045
|
+
const pr = prev.ref;
|
|
1046
|
+
const nr = next.ref;
|
|
1047
|
+
if (pr !== nr && isRefObject2(pr) && isRefObject2(nr)) {
|
|
1048
|
+
const { ref: _p, ...restPrev } = prev;
|
|
1049
|
+
const { ref: _n, ...restNext } = next;
|
|
1050
|
+
return [{ path: "ref", kind: "deep-equal", prev: pr, next: nr }, ...diffRecords(restPrev, restNext)];
|
|
1051
|
+
}
|
|
1052
|
+
return diffRecords(prev, next);
|
|
1053
|
+
}
|
|
990
1054
|
function analyze(fiber, alt, trackHooks) {
|
|
991
1055
|
const prevProps = alt.memoizedProps ?? {};
|
|
992
1056
|
const nextProps = fiber.memoizedProps ?? {};
|
|
993
|
-
const propChanges =
|
|
1057
|
+
const propChanges = diffProps(prevProps, nextProps);
|
|
994
1058
|
let stateChanges = [];
|
|
995
1059
|
let hookChanges = [];
|
|
996
1060
|
if (fiber.tag === ClassComponent) {
|
|
@@ -1003,7 +1067,7 @@ function analyze(fiber, alt, trackHooks) {
|
|
|
1003
1067
|
} else if (trackHooks) {
|
|
1004
1068
|
hookChanges = [...diffHooks(fiber, alt), ...diffContexts(fiber, alt)];
|
|
1005
1069
|
}
|
|
1006
|
-
const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore";
|
|
1070
|
+
const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore" && h.hook !== "useDeferredValue";
|
|
1007
1071
|
const causes = [];
|
|
1008
1072
|
if (propChanges.some(isGenuine2)) causes.push("props");
|
|
1009
1073
|
if (stateChanges.some(isGenuine2) || hookChanges.some((h) => isStateHook(h) && isGenuine2(h))) causes.push("state");
|
|
@@ -1043,6 +1107,24 @@ var lastCommit = null;
|
|
|
1043
1107
|
var EFFECT_LOOP_WINDOW_MS = 50;
|
|
1044
1108
|
var MAX_REPORTS_PER_COMMIT = 200;
|
|
1045
1109
|
var COMMIT_TIME_BUDGET_MS = 25;
|
|
1110
|
+
function isDeferredCatchUp(fiber) {
|
|
1111
|
+
const types = fiber._debugHookTypes;
|
|
1112
|
+
if (!types || !types.includes("useDeferredValue") || !fiber.alternate) return false;
|
|
1113
|
+
const first = fiber.memoizedState;
|
|
1114
|
+
if (!isHookList(first)) return false;
|
|
1115
|
+
const labels = hookLabelsFor(fiber, first);
|
|
1116
|
+
if (!labels) return false;
|
|
1117
|
+
let a = fiber.alternate.memoizedState;
|
|
1118
|
+
let b = first;
|
|
1119
|
+
for (let i = 0; a && b; a = a.next, b = b.next, i++) {
|
|
1120
|
+
if (labels[i] === "useDeferredValue" && !Object.is(a.memoizedState, b.memoizedState)) return true;
|
|
1121
|
+
}
|
|
1122
|
+
return false;
|
|
1123
|
+
}
|
|
1124
|
+
function isUnder(fiber, boundaries) {
|
|
1125
|
+
for (let f = fiber.return; f && f.tag !== HostRoot; f = f.return) if (f.tag === SuspenseComponent && boundaries.includes(f)) return true;
|
|
1126
|
+
return false;
|
|
1127
|
+
}
|
|
1046
1128
|
function onCommit(root, commitPriority) {
|
|
1047
1129
|
const s = getState();
|
|
1048
1130
|
if (!s.enabled) return;
|
|
@@ -1052,7 +1134,7 @@ function onCommit(root, commitPriority) {
|
|
|
1052
1134
|
const rendered = [];
|
|
1053
1135
|
const renderedNames = /* @__PURE__ */ new Set();
|
|
1054
1136
|
let hot = false;
|
|
1055
|
-
let
|
|
1137
|
+
let resolvedBoundaries = null;
|
|
1056
1138
|
const stack = [root.current];
|
|
1057
1139
|
while (stack.length) {
|
|
1058
1140
|
const fiber = stack.pop();
|
|
@@ -1062,7 +1144,7 @@ function onCommit(root, commitPriority) {
|
|
|
1062
1144
|
renderedNames.add(fiberName(fiber));
|
|
1063
1145
|
if (shouldTrack(fiberType(fiber), o)) rendered.push(fiber);
|
|
1064
1146
|
}
|
|
1065
|
-
if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null)
|
|
1147
|
+
if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) (resolvedBoundaries ?? (resolvedBoundaries = [])).push(fiber);
|
|
1066
1148
|
if (!alt || fiber.child !== alt.child) {
|
|
1067
1149
|
for (let c = fiber.child; c; c = c.sibling) stack.push(c);
|
|
1068
1150
|
}
|
|
@@ -1074,10 +1156,10 @@ function onCommit(root, commitPriority) {
|
|
|
1074
1156
|
let commitCause;
|
|
1075
1157
|
let afterCommit;
|
|
1076
1158
|
const inputDriven = commitPriority === "immediate" || commitPriority === "user-blocking";
|
|
1077
|
-
if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u))) {
|
|
1159
|
+
if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u)) && !rendered.some(isDeferredCatchUp)) {
|
|
1078
1160
|
commitCause = "effect-after-commit";
|
|
1079
1161
|
afterCommit = lastCommit.id;
|
|
1080
|
-
} else if (
|
|
1162
|
+
} else if (resolvedBoundaries) commitCause = "suspense-resolved";
|
|
1081
1163
|
if (rendered.length === 0) {
|
|
1082
1164
|
lastCommit = { id: s.nextCommitId, at, rendered: renderedNames };
|
|
1083
1165
|
return;
|
|
@@ -1129,6 +1211,9 @@ function onCommit(root, commitPriority) {
|
|
|
1129
1211
|
owner: ownerName(fiber),
|
|
1130
1212
|
path: componentPath(fiber),
|
|
1131
1213
|
memoized: isMemoizedFiber(fiber),
|
|
1214
|
+
compiled: isCompiledFiber(fiber),
|
|
1215
|
+
classComponent: fiber.tag === ClassComponent,
|
|
1216
|
+
revealed: resolvedBoundaries !== null && a.trigger === "parent" && isUnder(fiber, resolvedBoundaries),
|
|
1132
1217
|
selfDuration: durations ? durations.self : void 0,
|
|
1133
1218
|
treeDuration: durations ? durations.tree : void 0,
|
|
1134
1219
|
commitId,
|
|
@@ -1142,38 +1227,203 @@ function onCommit(root, commitPriority) {
|
|
|
1142
1227
|
}
|
|
1143
1228
|
}
|
|
1144
1229
|
|
|
1230
|
+
// src/causes.ts
|
|
1231
|
+
var isAncestorReport = (anc, r) => anc.path.length < r.path.length && r.path[anc.path.length] === anc.component && anc.path.every((p, i) => r.path[i] === p);
|
|
1232
|
+
var bump = (m, key, n = 1) => void m.set(key, (m.get(key) || 0) + n);
|
|
1233
|
+
function indexByComponent(reports) {
|
|
1234
|
+
const index = /* @__PURE__ */ new Map();
|
|
1235
|
+
for (const r of reports) {
|
|
1236
|
+
const list = index.get(r.component);
|
|
1237
|
+
if (list) list.push(r);
|
|
1238
|
+
else index.set(r.component, [r]);
|
|
1239
|
+
}
|
|
1240
|
+
return index;
|
|
1241
|
+
}
|
|
1242
|
+
function rootCauseOf(r, commitReports, index = indexByComponent(commitReports)) {
|
|
1243
|
+
let cur = r;
|
|
1244
|
+
const seen = /* @__PURE__ */ new Set([r]);
|
|
1245
|
+
while (cur.trigger === "parent" && cur.parent) {
|
|
1246
|
+
const parent = cur.parent;
|
|
1247
|
+
const candidates = index.get(parent.name);
|
|
1248
|
+
const p = candidates && candidates.find((x) => isAncestorReport(x, cur));
|
|
1249
|
+
if (!p || seen.has(p)) return { name: parent.name, trigger: parent.trigger, report: null };
|
|
1250
|
+
seen.add(p);
|
|
1251
|
+
cur = p;
|
|
1252
|
+
}
|
|
1253
|
+
return cur === r ? null : { name: cur.component, trigger: cur.trigger, report: cur };
|
|
1254
|
+
}
|
|
1255
|
+
function rootCausesOf(reports) {
|
|
1256
|
+
const roots = /* @__PURE__ */ new Map();
|
|
1257
|
+
const rootByReport = /* @__PURE__ */ new Map();
|
|
1258
|
+
const index = indexByComponent(reports);
|
|
1259
|
+
let avoidable = 0;
|
|
1260
|
+
let wasted = 0;
|
|
1261
|
+
for (const r of reports) {
|
|
1262
|
+
if (!r.avoidable) continue;
|
|
1263
|
+
avoidable++;
|
|
1264
|
+
if (typeof r.selfDuration === "number") wasted += r.selfDuration;
|
|
1265
|
+
const root = rootCauseOf(r, reports, index);
|
|
1266
|
+
const name = root ? root.name : r.parent && r.parent.name || "(unknown)";
|
|
1267
|
+
const trigger = root ? root.trigger : r.parent && r.parent.trigger || "parent";
|
|
1268
|
+
rootByReport.set(r, name);
|
|
1269
|
+
let agg = roots.get(name);
|
|
1270
|
+
if (!agg) {
|
|
1271
|
+
agg = { name, trigger, count: 0, components: /* @__PURE__ */ new Map() };
|
|
1272
|
+
roots.set(name, agg);
|
|
1273
|
+
}
|
|
1274
|
+
agg.count++;
|
|
1275
|
+
bump(agg.components, r.component);
|
|
1276
|
+
}
|
|
1277
|
+
return { roots: [...roots.values()].sort((a, b) => b.count - a.count), rootByReport, avoidable, wasted };
|
|
1278
|
+
}
|
|
1279
|
+
function groupByCommit(reports) {
|
|
1280
|
+
const out = /* @__PURE__ */ new Map();
|
|
1281
|
+
for (const r of reports) {
|
|
1282
|
+
if (typeof r.commitId !== "number" || r.commitId <= 0) continue;
|
|
1283
|
+
const list = out.get(r.commitId);
|
|
1284
|
+
if (list) list.push(r);
|
|
1285
|
+
else out.set(r.commitId, [r]);
|
|
1286
|
+
}
|
|
1287
|
+
return out;
|
|
1288
|
+
}
|
|
1289
|
+
function rankRootCauses(reports) {
|
|
1290
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1291
|
+
for (const commit of groupByCommit(reports).values()) {
|
|
1292
|
+
for (const root of rootCausesOf(commit).roots) {
|
|
1293
|
+
let agg = byName.get(root.name);
|
|
1294
|
+
if (!agg) {
|
|
1295
|
+
agg = { name: root.name, trigger: root.trigger, commits: 0, count: 0, components: /* @__PURE__ */ new Map() };
|
|
1296
|
+
byName.set(root.name, agg);
|
|
1297
|
+
}
|
|
1298
|
+
agg.commits++;
|
|
1299
|
+
agg.count += root.count;
|
|
1300
|
+
for (const [c, n] of root.components) bump(agg.components, c, n);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
return [...byName.values()].sort((a, b) => b.count - a.count || b.commits - a.commits || a.name.localeCompare(b.name));
|
|
1304
|
+
}
|
|
1305
|
+
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
1306
|
+
function formatRootCauses(reports, limit = 5) {
|
|
1307
|
+
const ranked = rankRootCauses(reports);
|
|
1308
|
+
const lines = ranked.slice(0, limit).map((root, i) => `${String(i + 1).padStart(2)}. <${root.name}> (${root.trigger}) started ${plural(root.commits, "commit")}, ${plural(root.count, "avoidable re-render")} (${[...root.components].map(([c, n]) => `<${c}>${n > 1 ? " x" + n : ""}`).join(", ")})`);
|
|
1309
|
+
if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
|
|
1310
|
+
return lines.join("\n");
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1145
1313
|
// src/fixes.ts
|
|
1146
|
-
var
|
|
1314
|
+
var MAX_FIX_REPORTS = 50;
|
|
1147
1315
|
var rootOf = (path) => path.split(/[.[]/)[0] || path;
|
|
1316
|
+
var identifier = (name) => /^[A-Za-z_$][\w$]*$/.test(name) ? name : "value";
|
|
1317
|
+
function shortValue(v, max = 60) {
|
|
1318
|
+
let s;
|
|
1319
|
+
try {
|
|
1320
|
+
s = JSON.stringify(v);
|
|
1321
|
+
} catch {
|
|
1322
|
+
s = String(v);
|
|
1323
|
+
}
|
|
1324
|
+
if (s === void 0) s = String(v);
|
|
1325
|
+
return s.length > max ? s.slice(0, max - 3) + "..." : s;
|
|
1326
|
+
}
|
|
1148
1327
|
function fixesFor(r) {
|
|
1149
1328
|
const out = [];
|
|
1150
|
-
const
|
|
1151
|
-
const
|
|
1329
|
+
const ownerName2 = r.owner || r.parent && r.parent.name || null;
|
|
1330
|
+
const propChanges = r.propChanges || [];
|
|
1331
|
+
const changes = [...propChanges, ...r.stateChanges || [], ...r.hookChanges || []];
|
|
1152
1332
|
if (r.avoidable && (changes.length === 0 || r.memoized === false)) {
|
|
1333
|
+
const identical = changes.length === 0;
|
|
1153
1334
|
out.push({
|
|
1154
1335
|
kind: "memo",
|
|
1155
1336
|
owner: r.component,
|
|
1156
1337
|
target: r.component,
|
|
1157
1338
|
prop: null,
|
|
1158
1339
|
label: `Wrap <${r.component}> in React.memo`,
|
|
1159
|
-
detail:
|
|
1340
|
+
detail: identical ? `<${r.component}> re-rendered with identical props because <${r.parent && r.parent.name || "its parent"}> re-rendered.` : `<${r.component}> is not memoized: fixing its props alone will not stop the re-render.`,
|
|
1341
|
+
snippet: `// ${r.component}
|
|
1342
|
+
import { memo } from 'react';
|
|
1343
|
+
|
|
1344
|
+
export const ${r.component} = memo(function ${r.component}(props) {
|
|
1345
|
+
// ...
|
|
1346
|
+
});
|
|
1347
|
+
// class components: extend PureComponent instead`
|
|
1160
1348
|
});
|
|
1161
1349
|
}
|
|
1162
|
-
for (const c of
|
|
1163
|
-
if (!
|
|
1350
|
+
for (const c of propChanges) {
|
|
1351
|
+
if (!AVOIDABLE_KINDS.has(c.kind)) continue;
|
|
1352
|
+
const owner = ownerName2 || "?";
|
|
1164
1353
|
const root = rootOf(c.path);
|
|
1354
|
+
const id = identifier(root);
|
|
1165
1355
|
if (root === "children" && (c.kind === "element" || c.kind === "deep-equal")) {
|
|
1166
|
-
out.push({
|
|
1356
|
+
out.push({
|
|
1357
|
+
kind: "children",
|
|
1358
|
+
owner,
|
|
1359
|
+
target: r.component,
|
|
1360
|
+
prop: "children",
|
|
1361
|
+
label: `memoize children of <${r.component}> in <${owner}>`,
|
|
1362
|
+
detail: `<${owner}> re-creates the children of <${r.component}> on every render; they have the same types and props each time.`,
|
|
1363
|
+
snippet: `// ${owner}
|
|
1364
|
+
import { useMemo } from 'react';
|
|
1365
|
+
|
|
1366
|
+
const children = useMemo(() => (
|
|
1367
|
+
<>{/* the same elements */}</>
|
|
1368
|
+
), [/* deps */]);
|
|
1369
|
+
|
|
1370
|
+
<${r.component}>{children}</${r.component}>
|
|
1371
|
+
|
|
1372
|
+
// static children: hoist them to module scope
|
|
1373
|
+
const STATIC = <em>hi</em>;`
|
|
1374
|
+
});
|
|
1167
1375
|
} else if (c.kind === "function") {
|
|
1168
|
-
out.push({
|
|
1376
|
+
out.push({
|
|
1377
|
+
kind: "useCallback",
|
|
1378
|
+
owner,
|
|
1379
|
+
target: r.component,
|
|
1380
|
+
prop: root,
|
|
1381
|
+
label: `useCallback(${root}) in <${owner}>`,
|
|
1382
|
+
detail: `prop "${c.path}" of <${r.component}> is a new function on every render of <${owner}>.`,
|
|
1383
|
+
snippet: `// ${owner}
|
|
1384
|
+
import { useCallback } from 'react';
|
|
1385
|
+
|
|
1386
|
+
const ${id} = useCallback((/* args */) => {
|
|
1387
|
+
// ...
|
|
1388
|
+
}, [/* deps */]);
|
|
1389
|
+
|
|
1390
|
+
<${r.component} ${root}={${id}} />`
|
|
1391
|
+
});
|
|
1169
1392
|
} else if (c.kind === "element") {
|
|
1170
|
-
out.push({
|
|
1393
|
+
out.push({
|
|
1394
|
+
kind: "useMemoElement",
|
|
1395
|
+
owner,
|
|
1396
|
+
target: r.component,
|
|
1397
|
+
prop: root,
|
|
1398
|
+
label: `memoize element prop ${root} in <${owner}>`,
|
|
1399
|
+
detail: `prop "${c.path}" of <${r.component}> is a new element with the same type and props on every render of <${owner}>.`,
|
|
1400
|
+
snippet: `// ${owner}
|
|
1401
|
+
import { useMemo } from 'react';
|
|
1402
|
+
|
|
1403
|
+
const ${id} = useMemo(() => ${shortValue(c.next, 40)}, [/* deps */]);
|
|
1404
|
+
// or pass it as children from a component that does not re-render`
|
|
1405
|
+
});
|
|
1171
1406
|
} else {
|
|
1172
|
-
|
|
1407
|
+
const isArray = Array.isArray(c.next);
|
|
1408
|
+
out.push({
|
|
1409
|
+
kind: "useMemo",
|
|
1410
|
+
owner,
|
|
1411
|
+
target: r.component,
|
|
1412
|
+
prop: root,
|
|
1413
|
+
label: `useMemo(${root}) in <${owner}>`,
|
|
1414
|
+
detail: `prop "${c.path}" of <${r.component}> is a new ${isArray ? "array" : "object"} with the same contents on every render of <${owner}>.`,
|
|
1415
|
+
snippet: `// ${owner}
|
|
1416
|
+
import { useMemo } from 'react';
|
|
1417
|
+
|
|
1418
|
+
const ${id} = useMemo(() => (${shortValue(c.next, 80)}), [/* deps */]);
|
|
1419
|
+
|
|
1420
|
+
// or, when it never changes, hoist it to module scope:
|
|
1421
|
+
const ${id.toUpperCase()} = ${shortValue(c.next, 80)};`
|
|
1422
|
+
});
|
|
1173
1423
|
}
|
|
1174
1424
|
}
|
|
1175
|
-
for (const c of [...r.stateChanges, ...r.hookChanges]) {
|
|
1176
|
-
const isContext = c.hook === "useContext";
|
|
1425
|
+
for (const c of [...r.stateChanges || [], ...r.hookChanges || []]) {
|
|
1426
|
+
const isContext = c.hook === "useContext" || /^useContext/.test(c.path);
|
|
1177
1427
|
const ctxName = isContext ? (/useContext\((.*)\)/.exec(c.path) || [])[1] || "Context" : "";
|
|
1178
1428
|
const providerOwner = isContext && c.provider && c.provider.component ? c.provider.component : null;
|
|
1179
1429
|
if (isContext && c.kind === "different" && c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys) {
|
|
@@ -1183,20 +1433,63 @@ function fixesFor(r) {
|
|
|
1183
1433
|
target: r.component,
|
|
1184
1434
|
prop: ctxName,
|
|
1185
1435
|
label: `split ${ctxName}${providerOwner ? ` in <${providerOwner}>` : ""}: only ${c.changedKeys.join(", ")} changed`,
|
|
1186
|
-
detail: `${c.changedKeys.
|
|
1436
|
+
detail: `${c.changedKeys.map((k) => `"${k}"`).join(", ")} of ${c.totalKeys} keys changed in ${ctxName}, yet every consumer (like <${r.component}>) re-rendered. Consumers that read the other keys re-render for nothing.`,
|
|
1437
|
+
snippet: `// ${providerOwner || "Provider"}
|
|
1438
|
+
// one context per independently-changing slice
|
|
1439
|
+
const ${identifier(ctxName)}Static = createContext(...);
|
|
1440
|
+
const ${identifier(ctxName)}${c.changedKeys.map((k) => k[0].toUpperCase() + k.slice(1)).join("")} = createContext(...);
|
|
1441
|
+
|
|
1442
|
+
// or keep one context and let consumers select a slice:
|
|
1443
|
+
const ${c.changedKeys[0]} = useContextSelector(${ctxName}, (v) => v.${c.changedKeys[0]});`
|
|
1187
1444
|
});
|
|
1188
1445
|
continue;
|
|
1189
1446
|
}
|
|
1190
|
-
if (!
|
|
1447
|
+
if (!AVOIDABLE_KINDS.has(c.kind)) continue;
|
|
1191
1448
|
if (isContext) {
|
|
1192
|
-
out.push({
|
|
1449
|
+
out.push({
|
|
1450
|
+
kind: "contextValue",
|
|
1451
|
+
owner: providerOwner || `${ctxName}.Provider`,
|
|
1452
|
+
target: r.component,
|
|
1453
|
+
prop: ctxName,
|
|
1454
|
+
label: `memoize the ${ctxName} provider value${providerOwner ? ` in <${providerOwner}>` : ""}`,
|
|
1455
|
+
detail: `<${r.component}> re-rendered because ${ctxName} produced a new value that is deep-equal to the previous one${providerOwner ? ` (Provider rendered by <${providerOwner}>)` : ""}.`,
|
|
1456
|
+
snippet: `// ${providerOwner || `where <${ctxName}.Provider> is rendered`}
|
|
1457
|
+
const value = useMemo(() => ({ /* ... */ }), [/* deps */]);
|
|
1458
|
+
<${ctxName}.Provider value={value}>`
|
|
1459
|
+
});
|
|
1193
1460
|
} else if (c.hook === "useSyncExternalStore") {
|
|
1194
1461
|
const chain = c.custom || [];
|
|
1195
1462
|
const redux = chain.some((n) => /^use(App)?Selector$/.test(n));
|
|
1196
1463
|
const zustand = !redux && chain.some((n) => /^use[A-Z]\w*Store$/.test(n) || n === "useStore" || n === "useBoundStore");
|
|
1197
|
-
|
|
1464
|
+
const via = chain.length ? ` via ${chain.join(" \u203A ")}` : "";
|
|
1465
|
+
out.push({
|
|
1466
|
+
kind: "storeSnapshot",
|
|
1467
|
+
owner: r.component,
|
|
1468
|
+
target: r.component,
|
|
1469
|
+
prop: c.path,
|
|
1470
|
+
label: redux ? `memoize the selector in <${r.component}>` : zustand ? `useShallow in <${r.component}>` : `stable getSnapshot in <${r.component}>`,
|
|
1471
|
+
detail: redux ? `the selector${via} returns a new object on every call, so the component re-renders on every store change.` : zustand ? `the store selector${via} returns a new object on every call, so the component re-renders on every store change.` : `${c.path}${via} returned a new reference with the same contents; getSnapshot must return a cached value.`,
|
|
1472
|
+
snippet: redux ? `// ${r.component}
|
|
1473
|
+
import { shallowEqual } from 'react-redux';
|
|
1474
|
+
const slice = useSelector(selectSlice, shallowEqual);
|
|
1475
|
+
// or memoize: const selectSlice = createSelector([selectA, selectB], (a, b) => ({ a, b }));` : zustand ? `// ${r.component}
|
|
1476
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
1477
|
+
const { a, b } = useStore(useShallow((s) => ({ a: s.a, b: s.b })));
|
|
1478
|
+
// or select a primitive: const a = useStore((s) => s.a);` : `// ${r.component}
|
|
1479
|
+
// getSnapshot must return the same reference while the data is unchanged
|
|
1480
|
+
const snapshot = useSyncExternalStore(subscribe, store.getSnapshot /* cached */);`
|
|
1481
|
+
});
|
|
1198
1482
|
} else {
|
|
1199
|
-
out.push({
|
|
1483
|
+
out.push({
|
|
1484
|
+
kind: "bailout",
|
|
1485
|
+
owner: r.component,
|
|
1486
|
+
target: r.component,
|
|
1487
|
+
prop: c.path,
|
|
1488
|
+
label: `bail out before setting ${c.path} in <${r.component}>`,
|
|
1489
|
+
detail: `${c.path} was set to a value deep-equal to the current one (new reference).`,
|
|
1490
|
+
snippet: `// ${r.component}
|
|
1491
|
+
setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
1492
|
+
});
|
|
1200
1493
|
}
|
|
1201
1494
|
}
|
|
1202
1495
|
return out;
|
|
@@ -1210,11 +1503,12 @@ function rankFixes(reports) {
|
|
|
1210
1503
|
const key = fixKey(f);
|
|
1211
1504
|
let agg = byKey.get(key);
|
|
1212
1505
|
if (!agg) {
|
|
1213
|
-
agg = { ...f, key, count: 0, components: {} };
|
|
1506
|
+
agg = { ...f, key, count: 0, components: {}, reports: [] };
|
|
1214
1507
|
byKey.set(key, agg);
|
|
1215
1508
|
}
|
|
1216
1509
|
agg.count++;
|
|
1217
1510
|
agg.components[r.component] = (agg.components[r.component] || 0) + 1;
|
|
1511
|
+
if (agg.reports.length < MAX_FIX_REPORTS) agg.reports.push(r);
|
|
1218
1512
|
}
|
|
1219
1513
|
}
|
|
1220
1514
|
return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
|
|
@@ -1224,6 +1518,8 @@ function formatFixes(reports, limit = 10) {
|
|
|
1224
1518
|
if (!ranked.length) return "No avoidable re-renders.";
|
|
1225
1519
|
const lines = ranked.slice(0, limit).map((f, i) => `${String(i + 1).padStart(2)}. ${f.label} (removes ${f.count}: ${Object.entries(f.components).map(([c, n]) => `<${c}>${n > 1 ? " x" + n : ""}`).join(", ")})`);
|
|
1226
1520
|
if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
|
|
1521
|
+
const roots = formatRootCauses(reports, 5);
|
|
1522
|
+
if (roots) lines.push("", "Root causes:", roots);
|
|
1227
1523
|
return lines.join("\n");
|
|
1228
1524
|
}
|
|
1229
1525
|
|
|
@@ -1322,7 +1618,7 @@ ${formatFixes(reports)}`);
|
|
|
1322
1618
|
};
|
|
1323
1619
|
}
|
|
1324
1620
|
|
|
1325
|
-
// src/
|
|
1621
|
+
// src/runner-report.ts
|
|
1326
1622
|
var OUT_ENV = "RERENDER_LENS_OUT";
|
|
1327
1623
|
function setupRerenderLens(options = {}, hooks = {}) {
|
|
1328
1624
|
ensureDevtoolsHook();
|