react-render-detective 0.5.0 → 0.6.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +2 -2
  3. package/dist/babel.cjs +2 -2
  4. package/dist/babel.d.cts +11 -0
  5. package/dist/babel.d.ts +11 -0
  6. package/dist/babel.js +1 -1
  7. package/dist/{chunk-DI5STJM4.js → chunk-3TZNM7JO.js} +66 -3
  8. package/dist/chunk-3TZNM7JO.js.map +1 -0
  9. package/dist/{chunk-HOTY7J3X.js → chunk-D7M4UC3Y.js} +26 -2
  10. package/dist/chunk-D7M4UC3Y.js.map +1 -0
  11. package/dist/{chunk-PHYYA67T.cjs → chunk-EXDRRUUQ.cjs} +69 -2
  12. package/dist/chunk-EXDRRUUQ.cjs.map +1 -0
  13. package/dist/{chunk-OSPTERGK.cjs → chunk-LCGXVRJR.cjs} +66 -3
  14. package/dist/chunk-LCGXVRJR.cjs.map +1 -0
  15. package/dist/{chunk-MCIQMYNR.js → chunk-QVHAC7AD.js} +69 -2
  16. package/dist/chunk-QVHAC7AD.js.map +1 -0
  17. package/dist/{chunk-JD4IJ4MN.cjs → chunk-RGT44QQI.cjs} +26 -2
  18. package/dist/chunk-RGT44QQI.cjs.map +1 -0
  19. package/dist/core.cjs +18 -18
  20. package/dist/core.d.cts +13 -3
  21. package/dist/core.d.ts +13 -3
  22. package/dist/core.js +1 -1
  23. package/dist/index.cjs +72 -274
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +35 -115
  26. package/dist/index.d.ts +35 -115
  27. package/dist/index.js +41 -237
  28. package/dist/index.js.map +1 -1
  29. package/dist/interactions.cjs +252 -0
  30. package/dist/interactions.cjs.map +1 -0
  31. package/dist/interactions.d.cts +110 -0
  32. package/dist/interactions.d.ts +110 -0
  33. package/dist/interactions.js +241 -0
  34. package/dist/interactions.js.map +1 -0
  35. package/dist/overlay.cjs +5 -5
  36. package/dist/overlay.js +2 -2
  37. package/dist/testing.d.cts +1 -1
  38. package/dist/testing.d.ts +1 -1
  39. package/dist/{types-gl13xwmN.d.cts → types-CRET8EhB.d.cts} +20 -2
  40. package/dist/{types-gl13xwmN.d.ts → types-CRET8EhB.d.ts} +20 -2
  41. package/dist/vite.cjs +2 -2
  42. package/dist/vite.js +1 -1
  43. package/package.json +6 -1
  44. package/dist/chunk-DI5STJM4.js.map +0 -1
  45. package/dist/chunk-HOTY7J3X.js.map +0 -1
  46. package/dist/chunk-JD4IJ4MN.cjs.map +0 -1
  47. package/dist/chunk-MCIQMYNR.js.map +0 -1
  48. package/dist/chunk-OSPTERGK.cjs.map +0 -1
  49. package/dist/chunk-PHYYA67T.cjs.map +0 -1
package/dist/index.d.ts CHANGED
@@ -1,96 +1,8 @@
1
- import { b as RenderEvent, j as Confidence, R as RenderReason, a as DetectiveOptions, D as DetectiveConfig, A as AppStats, d as ComponentStats } from './types-gl13xwmN.js';
2
- export { i as ComponentInfo, C as ContextChange, f as Diagnosis, I as Inspected, g as InspectionLimits, M as Mode, P as PropChange, k as PropChangeKind, h as PropValueType, c as RenderPhase, l as RenderTimings, e as Thresholds, T as TrackedStateChange } from './types-gl13xwmN.js';
1
+ import { j as Confidence, b as RenderEvent, R as RenderReason, a as DetectiveOptions, D as DetectiveConfig, A as AppStats, d as ComponentStats } from './types-CRET8EhB.js';
2
+ export { i as ComponentInfo, C as ContextChange, f as Diagnosis, I as Inspected, g as InspectionLimits, M as Mode, P as PropChange, k as PropChangeKind, h as PropValueType, c as RenderPhase, l as RenderTimings, S as SelectorChange, e as Thresholds, T as TrackedStateChange } from './types-CRET8EhB.js';
3
3
  import { RenderProfile } from './testing.js';
4
4
  import { ComponentType, ReactNode, Dispatch, SetStateAction } from 'react';
5
5
 
6
- /**
7
- * Interaction-scoped attribution.
8
- *
9
- * Performance is felt per interaction, not in aggregate — and INP is the metric
10
- * teams are actually judged on. This joins the two halves: the browser says a
11
- * keystroke took 240ms, and the render events say which components spent it and
12
- * why.
13
- *
14
- * Uses `PerformanceObserver` with `event` timing, a public browser API. Where it
15
- * is unsupported (older Safari, jsdom) interaction tracking simply stays empty
16
- * rather than guessing.
17
- */
18
- interface InteractionRecord {
19
- id: string;
20
- /** `click`, `keydown`, `pointerup`… */
21
- type: string;
22
- /** Best-effort description of what was interacted with. */
23
- target?: string;
24
- startTime: number;
25
- /** Browser-reported event duration — the number INP is computed from. */
26
- durationMs: number;
27
- /**
28
- * For a manually measured interaction: how long the synchronous action took.
29
- * Immune to throttling, unlike the full window, which waits on a frame.
30
- */
31
- handlerMs?: number;
32
- /** Renders committed inside this interaction's window. */
33
- renders: RenderEvent[];
34
- /** Sum of self durations for those renders. */
35
- renderTimeMs: number;
36
- /** Render time that no observable input change explains. */
37
- avoidableRenderTimeMs: number;
38
- }
39
- interface InteractionSummary {
40
- interaction: InteractionRecord;
41
- /** Components ordered by cost within this interaction. */
42
- contributors: Array<{
43
- component: string;
44
- source?: string;
45
- renders: number;
46
- totalMs: number;
47
- cause: string;
48
- }>;
49
- headline: string;
50
- nextStep: string;
51
- confidence: Confidence;
52
- }
53
- interface RawEventTiming {
54
- name: string;
55
- startTime: number;
56
- duration: number;
57
- target?: string;
58
- handlerMs?: number;
59
- }
60
- declare class InteractionTracker {
61
- private capacity;
62
- private records;
63
- private observer;
64
- private nextId;
65
- constructor(capacity?: number);
66
- /** Returns false when the browser cannot report event timing. */
67
- start(): boolean;
68
- stop(): void;
69
- /** Is the automatic path available in this browser? */
70
- get automatic(): boolean;
71
- /**
72
- * Time an interaction by hand.
73
- *
74
- * The automatic path depends on the Event Timing API, which Safari only
75
- * gained in 16.4 and which does not fire for synthetic input at all — so
76
- * anything driven by a test harness records nothing. This measures a specific
77
- * action instead, up to the paint that follows it, and needs no browser
78
- * support beyond `performance.now`.
79
- */
80
- measure<T>(label: string, action: () => T): T;
81
- /** Exposed for tests and for `measure`. */
82
- record(timing: RawEventTiming): InteractionRecord;
83
- clear(): void;
84
- /**
85
- * Joins render events to interactions by commit time. A render belongs to an
86
- * interaction when it committed between the event starting and shortly after
87
- * it finished — React commits just after the event handler returns.
88
- */
89
- attribute(events: RenderEvent[]): InteractionRecord[];
90
- }
91
- declare function summarise(record: InteractionRecord): InteractionSummary;
92
- declare function formatInteraction(summary: InteractionSummary): string;
93
-
94
6
  /**
95
7
  * "Where should I spend my next hour?"
96
8
  *
@@ -171,6 +83,31 @@ interface RenderDetectiveProps {
171
83
  */
172
84
  declare function RenderDetective({ name, children }: RenderDetectiveProps): ReactNode;
173
85
 
86
+ interface TrackSelectorOptions {
87
+ /** Readable label — a property path where one could be derived, else the call site. */
88
+ name?: string;
89
+ /** `File.tsx:12:3` of the call, supplied by the build plugin. */
90
+ source?: string;
91
+ }
92
+ /**
93
+ * Records a store value and returns it untouched.
94
+ *
95
+ * Pass-through by construction: the value is returned as given, and nothing
96
+ * here can change what the component receives.
97
+ */
98
+ declare function trackSelector<T>(value: T, options?: TrackSelectorOptions): T;
99
+ /**
100
+ * Wraps a store hook so its results are attributed. Signature-preserving: extra
101
+ * arguments (react-redux's equality function, for instance) pass straight
102
+ * through, because changing them would change the app's behaviour.
103
+ *
104
+ * const useSelector = createTrackedSelectorHook(useReduxSelector);
105
+ *
106
+ * The build plugin rewrites `useSelector` imports for you; use this directly for
107
+ * a custom store hook, or where the plugin cannot run.
108
+ */
109
+ declare function createTrackedSelectorHook<A extends unknown[], R>(hook: (...args: A) => R, options?: TrackSelectorOptions): (...args: A) => R;
110
+
174
111
  /**
175
112
  * In-component diagnostics without wrapping.
176
113
  *
@@ -231,6 +168,13 @@ interface Explanation {
231
168
  share: number;
232
169
  valueType: string;
233
170
  }>;
171
+ /** Store selectors that returned a new reference with identical contents. */
172
+ unstableSelectors: Array<{
173
+ name: string;
174
+ source?: string;
175
+ count: number;
176
+ share: number;
177
+ }>;
234
178
  averageSelfDuration: number;
235
179
  totalSelfDuration: number;
236
180
  potentiallyAvoidableRenders: number;
@@ -280,25 +224,6 @@ declare function explain(componentName: string): string | undefined;
280
224
  declare function explainStructured(componentName: string): Explanation | undefined;
281
225
  /** Snapshot render behaviour for regression testing. See `react-render-detective/testing`. */
282
226
  declare function getRenderProfile(scenario: string): RenderProfile;
283
- /**
284
- * Interactions, slowest first, with the renders that happened inside each.
285
- *
286
- * This is the bridge from render causality to what a user actually feels: the
287
- * browser reports how long the interaction took, and the render events say
288
- * which components spent that time and why.
289
- */
290
- declare function getInteractions(): InteractionRecord[];
291
- /** Structured analysis of one interaction. Defaults to the slowest recorded. */
292
- declare function explainInteractionStructured(id?: string): InteractionSummary | undefined;
293
- declare function explainInteraction(id?: string): string | undefined;
294
- declare function printInteractions(limit?: number): void;
295
- /**
296
- * Time one interaction explicitly, up to the paint that follows it.
297
- *
298
- * Needed wherever the automatic path cannot see: Safari before 16.4, and any
299
- * synthetic input, which never produces Event Timing entries.
300
- */
301
- declare function measureInteraction<T>(label: string, action: () => T): T;
302
227
  /**
303
228
  * Components ranked by estimated recoverable time — the triage view. Render
304
229
  * counts answer the wrong question; this answers "what should I fix first?".
@@ -323,13 +248,8 @@ declare const ReactRenderDetective: {
323
248
  explainStructured: typeof explainStructured;
324
249
  getOpportunities: typeof getOpportunities;
325
250
  printOpportunities: typeof printOpportunities;
326
- getInteractions: typeof getInteractions;
327
- explainInteraction: typeof explainInteraction;
328
- explainInteractionStructured: typeof explainInteractionStructured;
329
- printInteractions: typeof printInteractions;
330
- measureInteraction: typeof measureInteraction;
331
251
  getRenderProfile: typeof getRenderProfile;
332
252
  printStats: typeof printStats;
333
253
  };
334
254
 
335
- export { AppStats, ComponentStats, Confidence, DetectiveConfig, DetectiveOptions, type Explanation, type InteractionRecord, type InteractionSummary, InteractionTracker, type Opportunity, ReactRenderDetective, RenderDetective, type RenderDetectiveProps, RenderEvent, RenderReason, type TrackOptions, clear, configure, explain, explainEvents, explainInteraction, explainInteractionStructured, explainStructured, formatExplanation, formatInteraction, formatOpportunities, getComponentStats, getConfig, getEvents, getInteractions, getOpportunities, getRenderProfile, getStats, init, isEnabled, measureInteraction, printInteractions, printOpportunities, printStats, rankOpportunities, reset, subscribe, summarise as summariseInteraction, useRenderDiagnostics, useTrackedContextValue, useTrackedEffect, useTrackedState, withRenderDetective };
255
+ export { AppStats, ComponentStats, Confidence, DetectiveConfig, DetectiveOptions, type Explanation, type Opportunity, ReactRenderDetective, RenderDetective, type RenderDetectiveProps, RenderEvent, RenderReason, type TrackOptions, type TrackSelectorOptions, clear, configure, createTrackedSelectorHook, explain, explainEvents, explainStructured, formatExplanation, formatOpportunities, getComponentStats, getConfig, getEvents, getOpportunities, getRenderProfile, getStats, init, isEnabled, printOpportunities, printStats, rankOpportunities, reset, subscribe, trackSelector, useRenderDiagnostics, useTrackedContextValue, useTrackedEffect, useTrackedState, withRenderDetective };
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import { explainEvents, formatExplanation } from './chunk-HOTY7J3X.js';
2
- export { explainEvents, formatExplanation } from './chunk-HOTY7J3X.js';
3
- import { getDetective, inspect, shallowEqual, diffProps, formatInspected } from './chunk-DI5STJM4.js';
1
+ import { explainEvents, formatExplanation } from './chunk-D7M4UC3Y.js';
2
+ export { explainEvents, formatExplanation } from './chunk-D7M4UC3Y.js';
4
3
  import { profileFromEvents } from './chunk-LILK23YH.js';
4
+ import { getDetective, shallowEqual, inspect, diffProps, formatInspected } from './chunk-3TZNM7JO.js';
5
5
  import * as React from 'react';
6
- import { createContext, useContext, useState, useRef, useEffect, useLayoutEffect, useCallback, Profiler } from 'react';
6
+ import { createContext, useContext, useRef, useState, useEffect, useLayoutEffect, useCallback, Profiler } from 'react';
7
7
  import { jsx } from 'react/jsx-runtime';
8
8
 
9
9
  // src/console/style.ts
@@ -217,6 +217,8 @@ function label(event) {
217
217
  return "context update";
218
218
  case "state":
219
219
  return "state changed";
220
+ case "store":
221
+ return "store update";
220
222
  case "state-or-external":
221
223
  return "state or external store";
222
224
  case "unknown":
@@ -224,194 +226,6 @@ function label(event) {
224
226
  }
225
227
  }
226
228
 
227
- // src/core/interactions.ts
228
- var COMMIT_SLACK_MS = 100;
229
- var FALLBACK_CLOSE_MS = 50;
230
- var InteractionTracker = class {
231
- constructor(capacity = 50) {
232
- this.capacity = capacity;
233
- this.records = [];
234
- this.nextId = 0;
235
- }
236
- /** Returns false when the browser cannot report event timing. */
237
- start() {
238
- if (this.observer) return true;
239
- const PO = globalThis.PerformanceObserver;
240
- const supported = PO?.supportedEntryTypes?.includes("event");
241
- if (!PO || !supported) return false;
242
- try {
243
- const observer = new PO((list) => {
244
- for (const entry of list.getEntries()) {
245
- const timing = entry;
246
- this.record({
247
- name: timing.name,
248
- startTime: timing.startTime,
249
- duration: timing.duration,
250
- target: describeTarget(timing.target)
251
- });
252
- }
253
- });
254
- observer.observe({ type: "event", buffered: true, durationThreshold: 16 });
255
- this.observer = observer;
256
- return true;
257
- } catch {
258
- return false;
259
- }
260
- }
261
- stop() {
262
- this.observer?.disconnect();
263
- this.observer = void 0;
264
- }
265
- /** Is the automatic path available in this browser? */
266
- get automatic() {
267
- return this.observer !== void 0;
268
- }
269
- /**
270
- * Time an interaction by hand.
271
- *
272
- * The automatic path depends on the Event Timing API, which Safari only
273
- * gained in 16.4 and which does not fire for synthetic input at all — so
274
- * anything driven by a test harness records nothing. This measures a specific
275
- * action instead, up to the paint that follows it, and needs no browser
276
- * support beyond `performance.now`.
277
- */
278
- measure(label2, action) {
279
- const startTime = now();
280
- let handlerMs = 0;
281
- let finished = false;
282
- const finish = () => {
283
- if (finished) return;
284
- finished = true;
285
- this.record({ name: label2, startTime, duration: now() - startTime, handlerMs });
286
- };
287
- let result;
288
- try {
289
- result = action();
290
- handlerMs = now() - startTime;
291
- } catch (error) {
292
- handlerMs = now() - startTime;
293
- finish();
294
- throw error;
295
- }
296
- const raf = globalThis.requestAnimationFrame;
297
- if (raf) raf(() => raf(finish));
298
- setTimeout(finish, FALLBACK_CLOSE_MS);
299
- return result;
300
- }
301
- /** Exposed for tests and for `measure`. */
302
- record(timing) {
303
- const record = {
304
- id: `interaction_${++this.nextId}`,
305
- type: timing.name,
306
- target: timing.target,
307
- startTime: timing.startTime,
308
- durationMs: timing.duration,
309
- handlerMs: timing.handlerMs,
310
- renders: [],
311
- renderTimeMs: 0,
312
- avoidableRenderTimeMs: 0
313
- };
314
- this.records.push(record);
315
- if (this.records.length > this.capacity) this.records.shift();
316
- return record;
317
- }
318
- clear() {
319
- this.records.length = 0;
320
- }
321
- /**
322
- * Joins render events to interactions by commit time. A render belongs to an
323
- * interaction when it committed between the event starting and shortly after
324
- * it finished — React commits just after the event handler returns.
325
- */
326
- attribute(events) {
327
- for (const record of this.records) {
328
- const from = record.startTime;
329
- const to = record.startTime + record.durationMs + COMMIT_SLACK_MS;
330
- record.renders = events.filter((e) => e.timings.commitTime >= from && e.timings.commitTime <= to);
331
- record.renderTimeMs = record.renders.reduce((a, e) => a + e.timings.selfDuration, 0);
332
- record.avoidableRenderTimeMs = record.renders.filter((e) => e.diagnosis.potentiallyAvoidable).reduce((a, e) => a + e.timings.selfDuration, 0);
333
- }
334
- return [...this.records].sort((a, b) => b.durationMs - a.durationMs);
335
- }
336
- };
337
- function summarise(record) {
338
- const byComponent = /* @__PURE__ */ new Map();
339
- for (const event of record.renders) {
340
- const entry = byComponent.get(event.component.name) ?? {
341
- renders: 0,
342
- totalMs: 0,
343
- source: event.component.source,
344
- cause: event.diagnosis.reason
345
- };
346
- entry.renders++;
347
- entry.totalMs += event.timings.selfDuration;
348
- byComponent.set(event.component.name, entry);
349
- }
350
- const contributors = [...byComponent.entries()].map(([component, v]) => ({ component, source: v.source, renders: v.renders, totalMs: v.totalMs, cause: v.cause })).sort((a, b) => b.totalMs - a.totalMs);
351
- const top = contributors[0];
352
- const accounted = (record.handlerMs ?? record.durationMs) + record.renderTimeMs;
353
- const idleWindow = record.handlerMs !== void 0 && record.durationMs > accounted * 3 && record.durationMs - accounted > 100;
354
- const effectiveMs = idleWindow ? accounted : record.durationMs;
355
- const share = effectiveMs > 0 ? record.renderTimeMs / effectiveMs : 0;
356
- let headline;
357
- let nextStep;
358
- let confidence = "medium";
359
- if (idleWindow) {
360
- headline = `${record.type}: ${fmt(record.handlerMs ?? 0)} in the handler and ${fmt(record.renderTimeMs)} rendering. The measured window was ${fmt(record.durationMs)}, but most of that was the page waiting for a frame \u2014 ignore it.`;
361
- nextStep = record.renderTimeMs > (record.handlerMs ?? 0) ? `Rendering dominates the real work${top ? `; start with ${top.component}` : ""}.` : "The handler itself costs more than rendering. Profile the handler, not React.";
362
- return { interaction: record, contributors, headline, nextStep, confidence: "medium" };
363
- }
364
- if (record.renders.length === 0) {
365
- headline = `${record.type} took ${fmt(record.durationMs)}, and no instrumented component rendered inside it.`;
366
- nextStep = "The cost is somewhere other than React rendering \u2014 an event handler, a layout, or an uninstrumented component. Instrument more of the tree to narrow it down.";
367
- confidence = "low";
368
- } else if (share >= 0.4 && record.avoidableRenderTimeMs > 0) {
369
- headline = `${record.type} took ${fmt(record.durationMs)}; ${fmt(record.renderTimeMs)} of it was rendering, and ${fmt(record.avoidableRenderTimeMs)} of that had no input change to explain it.`;
370
- nextStep = top ? `Start with ${top.component}${top.source ? ` (${top.source})` : ""} \u2014 ${fmt(top.totalMs)} across ${top.renders} render${top.renders === 1 ? "" : "s"}.` : "Look at the top contributor below.";
371
- confidence = "high";
372
- } else if (share >= 0.4) {
373
- headline = `${record.type} took ${fmt(record.durationMs)}; ${fmt(record.renderTimeMs)} of it was rendering, all of it explained by real input changes.`;
374
- nextStep = "This is genuine work. Make the renders cheaper rather than fewer \u2014 or do less of it per interaction.";
375
- confidence = "high";
376
- } else {
377
- headline = `${record.type} took ${fmt(record.durationMs)}, but only ${fmt(record.renderTimeMs)} was React rendering.`;
378
- nextStep = "Most of the cost is outside rendering \u2014 event handlers, layout or paint. A browser profile will show more than this tool can.";
379
- confidence = "medium";
380
- }
381
- return { interaction: record, contributors, headline, nextStep, confidence };
382
- }
383
- function formatInteraction(summary) {
384
- const { interaction: i } = summary;
385
- const lines = [
386
- `${i.type}${i.target ? ` on ${i.target}` : ""} ${fmt(i.durationMs)}`,
387
- "",
388
- summary.headline
389
- ];
390
- if (summary.contributors.length > 0) {
391
- lines.push("", "Rendering inside this interaction");
392
- for (const c of summary.contributors.slice(0, 8)) {
393
- lines.push(
394
- ` ${c.component.padEnd(22)} ${String(c.renders).padStart(4)} render(s) ${fmt(c.totalMs).padStart(8)} ${c.cause}`
395
- );
396
- }
397
- }
398
- lines.push("", `Next step`, ` ${summary.nextStep}`, "", `Confidence: ${summary.confidence}`);
399
- return lines.join("\n");
400
- }
401
- function describeTarget(target) {
402
- if (!target || typeof target !== "object") return void 0;
403
- const el = target;
404
- if (!el.tagName) return void 0;
405
- const tag = el.tagName.toLowerCase();
406
- if (el.id) return `${tag}#${el.id}`;
407
- const className = typeof el.className === "string" ? el.className.trim().split(/\s+/)[0] : void 0;
408
- if (className) return `${tag}.${className}`;
409
- const text = el.textContent?.trim().slice(0, 20);
410
- return text ? `${tag} "${text}"` : tag;
411
- }
412
- var fmt = (ms) => `${ms.toFixed(1)}ms`;
413
- var now = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
414
-
415
229
  // src/core/opportunities.ts
416
230
  var DEFAULT_MIN_SAVING_MS = 1;
417
231
  function rankOpportunities({ events, lifecycles, minSavingMs = DEFAULT_MIN_SAVING_MS }) {
@@ -530,6 +344,38 @@ function RenderDetective({ name, children }) {
530
344
  const { node, onRender } = useInstrumentedNode(name, { children });
531
345
  return renderInstrumented(node, onRender, children);
532
346
  }
347
+ var useOwnerId = typeof React.useId === "function" ? React.useId : () => "";
348
+ function trackSelector(value, options = {}) {
349
+ const detective = getDetective();
350
+ const node = useContext(AncestryContext);
351
+ const ownerId = useOwnerId();
352
+ const previous = useRef(void 0);
353
+ if (!detective.enabled) return value;
354
+ if (node && node.stateOwner === void 0) node.stateOwner = ownerId;
355
+ const owns = !node || node.stateOwner === ownerId;
356
+ const prior = previous.current;
357
+ previous.current = { value };
358
+ if (!prior || !node || !owns) return value;
359
+ if (Object.is(prior.value, value)) return value;
360
+ try {
361
+ const equal = shallowEqual(prior.value, value, detective.config);
362
+ detective.recordSelectorChange(node, {
363
+ name: options.name ?? options.source ?? "selector",
364
+ source: options.source,
365
+ referenceOnly: equal === true,
366
+ previous: inspect(prior.value, detective.config.inspection),
367
+ current: inspect(value, detective.config.inspection)
368
+ });
369
+ } catch {
370
+ }
371
+ return value;
372
+ }
373
+ function createTrackedSelectorHook(hook, options = {}) {
374
+ return function useTrackedSelector(...args) {
375
+ const value = hook(...args);
376
+ return trackSelector(value, options);
377
+ };
378
+ }
533
379
  function useRenderDiagnostics(name, props) {
534
380
  const detective = getDetective();
535
381
  const parent = useContext(AncestryContext);
@@ -564,11 +410,11 @@ function useRenderDiagnostics(name, props) {
564
410
  }
565
411
  return last.current;
566
412
  }
567
- var useOwnerId = typeof React.useId === "function" ? React.useId : () => "";
413
+ var useOwnerId2 = typeof React.useId === "function" ? React.useId : () => "";
568
414
  function useTrackedState(name, initial) {
569
415
  const detective = getDetective();
570
416
  const node = useContext(AncestryContext);
571
- const ownerId = useOwnerId();
417
+ const ownerId = useOwnerId2();
572
418
  const [state, setState] = useState(initial);
573
419
  const previous = useRef(state);
574
420
  if (node && node.stateOwner === void 0) node.stateOwner = ownerId;
@@ -629,21 +475,12 @@ function isRecord(v) {
629
475
 
630
476
  // src/index.ts
631
477
  var REPORTER = /* @__PURE__ */ Symbol.for("react-render-detective.reporter");
632
- var INTERACTIONS = /* @__PURE__ */ Symbol.for("react-render-detective.interactions");
633
- function tracker() {
634
- const g = globalThis;
635
- if (!g[INTERACTIONS]) g[INTERACTIONS] = new InteractionTracker();
636
- return g[INTERACTIONS];
637
- }
638
478
  function init(options = {}) {
639
479
  const detective = getDetective();
640
480
  detective.init(options);
641
481
  const g = globalThis;
642
482
  g[REPORTER]?.();
643
483
  g[REPORTER] = void 0;
644
- if (detective.enabled) {
645
- tracker().start();
646
- }
647
484
  if (detective.enabled && detective.config.mode !== "silent") {
648
485
  g[REPORTER] = attachConsoleReporter(detective);
649
486
  } else if (!detective.enabled) {
@@ -675,14 +512,11 @@ function subscribe(listener) {
675
512
  }
676
513
  function clear() {
677
514
  getDetective().clear();
678
- tracker().clear();
679
515
  }
680
516
  function reset() {
681
517
  const g = globalThis;
682
518
  g[REPORTER]?.();
683
519
  g[REPORTER] = void 0;
684
- g[INTERACTIONS]?.stop();
685
- g[INTERACTIONS] = void 0;
686
520
  getDetective().reset();
687
521
  }
688
522
  function explain(componentName2) {
@@ -697,31 +531,6 @@ function getRenderProfile(scenario) {
697
531
  for (const stats of getDetective().getComponentStats()) remounts[stats.name] = stats.remountCount;
698
532
  return profileFromEvents(scenario, getEvents(), remounts);
699
533
  }
700
- function getInteractions() {
701
- return tracker().attribute(getEvents());
702
- }
703
- function explainInteractionStructured(id) {
704
- const records = getInteractions();
705
- const record = id ? records.find((r) => r.id === id) : records[0];
706
- return record ? summarise(record) : void 0;
707
- }
708
- function explainInteraction(id) {
709
- const summary = explainInteractionStructured(id);
710
- return summary ? formatInteraction(summary) : void 0;
711
- }
712
- function printInteractions(limit = 5) {
713
- const records = getInteractions().slice(0, limit);
714
- if (records.length === 0) {
715
- console.log(
716
- tracker().automatic ? "No interactions recorded yet. Event timing is working \u2014 nothing has taken longer than 16ms.\nSynthetic clicks from a test harness never produce these entries; use measureInteraction() there." : "This browser does not report event timing (Safari before 16.4, jsdom).\nUse measureInteraction(label, fn) to time interactions by hand."
717
- );
718
- return;
719
- }
720
- console.log(records.map((r) => formatInteraction(summarise(r))).join("\n\n"));
721
- }
722
- function measureInteraction(label2, action) {
723
- return tracker().measure(label2, action);
724
- }
725
534
  function getOpportunities(limit = 10) {
726
535
  const detective = getDetective();
727
536
  const lifecycles = /* @__PURE__ */ new Map();
@@ -780,15 +589,10 @@ var ReactRenderDetective = {
780
589
  explainStructured,
781
590
  getOpportunities,
782
591
  printOpportunities,
783
- getInteractions,
784
- explainInteraction,
785
- explainInteractionStructured,
786
- printInteractions,
787
- measureInteraction,
788
592
  getRenderProfile,
789
593
  printStats
790
594
  };
791
595
 
792
- export { InteractionTracker, ReactRenderDetective, RenderDetective, clear, configure, explain, explainInteraction, explainInteractionStructured, explainStructured, formatInteraction, formatOpportunities, getComponentStats, getConfig, getEvents, getInteractions, getOpportunities, getRenderProfile, getStats, init, isEnabled, measureInteraction, printInteractions, printOpportunities, printStats, rankOpportunities, reset, subscribe, summarise as summariseInteraction, useRenderDiagnostics, useTrackedContextValue, useTrackedEffect, useTrackedState, withRenderDetective };
596
+ export { ReactRenderDetective, RenderDetective, clear, configure, createTrackedSelectorHook, explain, explainStructured, formatOpportunities, getComponentStats, getConfig, getEvents, getOpportunities, getRenderProfile, getStats, init, isEnabled, printOpportunities, printStats, rankOpportunities, reset, subscribe, trackSelector, useRenderDiagnostics, useTrackedContextValue, useTrackedEffect, useTrackedState, withRenderDetective };
793
597
  //# sourceMappingURL=index.js.map
794
598
  //# sourceMappingURL=index.js.map