Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Type check (src)
run: npm run typecheck

- name: Type check (node/vite)
run: npm run typecheck:node

- name: Build extension
run: npm run build

Expand Down
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@ dist
.sisyphus
.agent
worker
node_modules
node_modules
*.tsbuildinfo

# macOS noise
.DS_Store
Empty file added bench/baselines/.gitkeep
Empty file.
Empty file added bench/results/.gitkeep
Empty file.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"fixture:basic:dev": "npm --prefix test/fixtures/basic run dev"
"fixture:basic:dev": "npm --prefix test/fixtures/basic run dev",
"typecheck": "tsc --noEmit",
"typecheck:node": "tsc -p tsconfig.node.json --noEmit",
"bench": "vitest bench --run"
},
"devDependencies": {
"@resvg/resvg-js": "^2.6.2",
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/IssueCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@

const mockInfoIssue: Issue = {
id: 'issue-3',
type: 'DEV_MODE_IN_PROD',
type: 'SLOW_RENDER',
severity: 'info',
component: 'App',
message: 'React is running in development mode',
Expand Down Expand Up @@ -138,7 +138,7 @@
const header = screen.getByText('Missing Key in List').closest('.issue-header');
if (header) fireEvent.click(header);

expect(screen.getByText('💡 Suggestion:')).toBeInTheDocument();

Check failure on line 141 in src/__tests__/IssueCard.test.tsx

View workflow job for this annotation

GitHub Actions / Build + Test

src/__tests__/IssueCard.test.tsx > IssueCard > Expansion > shows suggestion when expanded

TestingLibraryElementError: Unable to find an element with the text: 💡 Suggestion:. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body> <div> <div class="issue-card severity-error" style="border-left-color: rgb(255, 68, 68);" > <div class="issue-header" > <span class="issue-icon indicator-dot indicator-dot--error" /> <div class="issue-info" > <div class="issue-title-row" > <h4 class="issue-title" > Missing Key in List </h4> <span class="severity-badge" style="background-color: rgba(255, 68, 68, 0.1); color: rgb(255, 68, 68); border: 1px solid rgb(255, 68, 68);" > Error </span> </div> <div class="issue-location" > <span class="element-type" > li </span> <span class="issue-component" > in <strong> UserList </strong> </span> <span class="component-path" > ( App → Dashboard → UserList ) </span> </div> </div> <button class="expand-button" > ▼ </button> </div> <div class="issue-details" > <p class="issue-message" > List items are missing key props which can cause incorrect rendering </p> <div class="issue-elements" > <strong> Affected Elements: </strong> <div class="elements-list" > <div class="element-item missing-key" > <span class="el-index" > [ 0 ] </span> <span class="el-type" > li </span> <span class="el-key null" > key= null </span> </div> <div class="element-item missing-key" > <span class="el-index" > [ 1 ] </span> <span class="el-type" > li </span> <span class="el-key null" > key= null </span> </div> <div class="element-item missing-key" > <span class="el-index" > [ 2 ] </span> <span class="el-type" > li </span> <span class="el-key null" > key= null </span> </div> </div> </div> <div class="issue-why" > <strong> Why this matters: </strong> <p> Keys help React identify which items have changed, are added, or removed. </p> </div> <div class="issue-suggestion" > <strong> <span class="action-badge action-badge--suggestion" /> Suggestion: </strong> <p> Add a unique key prop to each list item </
expect(screen.getByText(/Add a unique key prop/)).toBeInTheDocument();
});
});
Expand Down Expand Up @@ -194,7 +194,7 @@
const header = screen.getByText('Stale Closure Detected').closest('.issue-header');
if (header) fireEvent.click(header);

expect(screen.getByText('🔍 Closure Timeline:')).toBeInTheDocument();

Check failure on line 197 in src/__tests__/IssueCard.test.tsx

View workflow job for this annotation

GitHub Actions / Build + Test

src/__tests__/IssueCard.test.tsx > IssueCard > Stale Closure Info > displays closure timeline

TestingLibraryElementError: Unable to find an element with the text: 🔍 Closure Timeline:. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body> <div> <div class="issue-card severity-error" style="border-left-color: rgb(255, 68, 68);" > <div class="issue-header" > <span class="issue-icon indicator-dot indicator-dot--error" /> <div class="issue-info" > <div class="issue-title-row" > <h4 class="issue-title" > Stale Closure Detected </h4> <span class="severity-badge" style="background-color: rgba(255, 68, 68, 0.1); color: rgb(255, 68, 68); border: 1px solid rgb(255, 68, 68);" > Error </span> </div> <div class="issue-location" > <span class="issue-component" > in <strong> Timer </strong> </span> <span class="component-path" > ( App → Timer ) </span> </div> </div> <button class="expand-button" > ▼ </button> </div> <div class="issue-details" > <p class="issue-message" > Callback is using stale closure values </p> <div class="closure-info" > <strong> <span class="action-badge action-badge--search" /> Closure Timeline: </strong> <div class="closure-timeline" > <div class="timeline-item created" > <span class="timeline-badge" > Created </span> <span class="timeline-detail" > Render # 1 </span> </div> <div class="timeline-arrow" > → </div> <div class="timeline-item executed" > <span class="timeline-badge warning" > Executed </span> <span class="timeline-detail" > Render # 10 </span> </div> </div> <div class="closure-details" > <div class="closure-row" > <span class="closure-label" > Function: </span> <code class="closure-value" > handleTick () </code> </div> <div class="closure-row" > <span class="closure-label" > Type: </span> <span class="closure-type type-setInterval" > setInterval </span> </div> <div class="closure-row" > <span class="closure-label" > Renders behind: </span> <span class="closure-stale-count" > 9 render(s) </span> </div> </div> <div class="captured-values" > <strong> Potentially stale v
});

it('shows created and executed render numbers', () => {
Expand Down Expand Up @@ -253,7 +253,7 @@
const header = screen.getByText('Missing Key in List').closest('.issue-header');
if (header) fireEvent.click(header);

expect(screen.getByText('📚 Learn more')).toBeInTheDocument();

Check failure on line 256 in src/__tests__/IssueCard.test.tsx

View workflow job for this annotation

GitHub Actions / Build + Test

src/__tests__/IssueCard.test.tsx > IssueCard > Learn More Link > displays learn more link when available

TestingLibraryElementError: Unable to find an element with the text: 📚 Learn more. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body> <div> <div class="issue-card severity-error" style="border-left-color: rgb(255, 68, 68);" > <div class="issue-header" > <span class="issue-icon indicator-dot indicator-dot--error" /> <div class="issue-info" > <div class="issue-title-row" > <h4 class="issue-title" > Missing Key in List </h4> <span class="severity-badge" style="background-color: rgba(255, 68, 68, 0.1); color: rgb(255, 68, 68); border: 1px solid rgb(255, 68, 68);" > Error </span> </div> <div class="issue-location" > <span class="element-type" > li </span> <span class="issue-component" > in <strong> UserList </strong> </span> <span class="component-path" > ( App → Dashboard → UserList ) </span> </div> </div> <button class="expand-button" > ▼ </button> </div> <div class="issue-details" > <p class="issue-message" > List items are missing key props which can cause incorrect rendering </p> <div class="issue-elements" > <strong> Affected Elements: </strong> <div class="elements-list" > <div class="element-item missing-key" > <span class="el-index" > [ 0 ] </span> <span class="el-type" > li </span> <span class="el-key null" > key= null </span> </div> <div class="element-item missing-key" > <span class="el-index" > [ 1 ] </span> <span class="el-type" > li </span> <span class="el-key null" > key= null </span> </div> <div class="element-item missing-key" > <span class="el-index" > [ 2 ] </span> <span class="el-type" > li </span> <span class="el-key null" > key= null </span> </div> </div> </div> <div class="issue-why" > <strong> Why this matters: </strong> <p> Keys help React identify which items have changed, are added, or removed. </p> </div> <div class="issue-suggestion" > <strong> <span class="action-badge action-badge--suggestion" /> Suggestion: </strong> <p> Add a unique key prop to each list item </p
});

it('link opens in new tab', () => {
Expand All @@ -262,7 +262,7 @@
const header = screen.getByText('Missing Key in List').closest('.issue-header');
if (header) fireEvent.click(header);

const link = screen.getByText('📚 Learn more');

Check failure on line 265 in src/__tests__/IssueCard.test.tsx

View workflow job for this annotation

GitHub Actions / Build + Test

src/__tests__/IssueCard.test.tsx > IssueCard > Learn More Link > link opens in new tab

TestingLibraryElementError: Unable to find an element with the text: 📚 Learn more. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body> <div> <div class="issue-card severity-error" style="border-left-color: rgb(255, 68, 68);" > <div class="issue-header" > <span class="issue-icon indicator-dot indicator-dot--error" /> <div class="issue-info" > <div class="issue-title-row" > <h4 class="issue-title" > Missing Key in List </h4> <span class="severity-badge" style="background-color: rgba(255, 68, 68, 0.1); color: rgb(255, 68, 68); border: 1px solid rgb(255, 68, 68);" > Error </span> </div> <div class="issue-location" > <span class="element-type" > li </span> <span class="issue-component" > in <strong> UserList </strong> </span> <span class="component-path" > ( App → Dashboard → UserList ) </span> </div> </div> <button class="expand-button" > ▼ </button> </div> <div class="issue-details" > <p class="issue-message" > List items are missing key props which can cause incorrect rendering </p> <div class="issue-elements" > <strong> Affected Elements: </strong> <div class="elements-list" > <div class="element-item missing-key" > <span class="el-index" > [ 0 ] </span> <span class="el-type" > li </span> <span class="el-key null" > key= null </span> </div> <div class="element-item missing-key" > <span class="el-index" > [ 1 ] </span> <span class="el-type" > li </span> <span class="el-key null" > key= null </span> </div> <div class="element-item missing-key" > <span class="el-index" > [ 2 ] </span> <span class="el-type" > li </span> <span class="el-key null" > key= null </span> </div> </div> </div> <div class="issue-why" > <strong> Why this matters: </strong> <p> Keys help React identify which items have changed, are added, or removed. </p> </div> <div class="issue-suggestion" > <strong> <span class="action-badge action-badge--suggestion" /> Suggestion: </strong> <p> Add a unique key prop to each list item </p
expect(link.getAttribute('target')).toBe('_blank');
expect(link.getAttribute('rel')).toBe('noopener noreferrer');
});
Expand Down
16 changes: 8 additions & 8 deletions src/__tests__/UIStateTab.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,11 @@ const mockUIIssues: Issue[] = [
},
{
id: 'issue-3',
type: 'DIRECT_STATE_MUTATION',
type: 'MISSING_KEY',
severity: 'error',
component: 'Counter',
message: 'State object was mutated directly',
suggestion: 'Use setState with a new object or spread operator',
code: 'state.count++ // Wrong!\nsetState({ count: state.count + 1 }) // Correct',
message: 'Counter list items are missing key props',
suggestion: 'Add unique key prop to each counter item',
timestamp: Date.now(),
},
];
Expand Down Expand Up @@ -105,17 +104,18 @@ describe('UIStateTab', () => {

it('filters MISSING_KEY issues', () => {
render(<UIStateTab issues={mockUIIssues} onClear={mockOnClear} />);
expect(screen.getByText('Missing Key in List')).toBeInTheDocument();
expect(screen.getAllByText('Missing Key in List').length).toBeGreaterThan(0);
});

it('filters INDEX_AS_KEY issues', () => {
render(<UIStateTab issues={mockUIIssues} onClear={mockOnClear} />);
expect(screen.getByText('Index Used as Key')).toBeInTheDocument();
});

it('filters DIRECT_STATE_MUTATION issues', () => {
render(<UIStateTab issues={mockUIIssues} onClear={mockOnClear} />);
expect(screen.getByText('Direct State Mutation')).toBeInTheDocument();
it('filters MISSING_KEY issues (from multiple fixtures)', () => {
const singleIssue = [mockUIIssues[0]];
render(<UIStateTab issues={singleIssue} onClear={mockOnClear} />);
expect(screen.getByText('Missing Key in List')).toBeInTheDocument();
});
});

Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/debugging-scenarios.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -338,10 +338,10 @@ const OptimizedChild = React.memo(function OptimizedChild({
* Scenario 2: INDEX_AS_KEY warning in UI & State tab
* Scenario 3: STALE_CLOSURE warning in Side Effects tab
* Scenario 4: MISSING_CLEANUP warning in Side Effects tab
* Scenario 5: UNNECESSARY_RERENDER in Performance tab
* Scenario 5: (removed ghost type) in Performance tab
* Scenario 6: SLOW_RENDER (>16ms) in Performance tab
* Scenario 7: Memory growth in Memory tab
* Scenario 8: DIRECT_STATE_MUTATION in UI & State tab
* Scenario 8: (removed ghost type) in UI & State tab
* Scenario 9: INFINITE_LOOP_RISK in Side Effects tab
* Scenario 10: Clean - no warnings (reference for good code)
*/
Expand Down
51 changes: 31 additions & 20 deletions src/inject/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// @ts-ignore: WeakRef is available in all modern browsers (ES2021+)
declare class WeakRef<T extends object> { constructor(target: T); deref(): T | undefined; }

import { installCleanupInterval, uninstallCleanupInterval } from './lifecycle';

(function() {
'use strict';

Expand All @@ -11,10 +13,6 @@ declare class WeakRef<T extends object> { constructor(target: T); deref(): T | u
// Global debugger enable/disable flag (default: OFF for performance)
let debuggerEnabled = false;

// Grace period: skip fiber commits for first N ms after enabling debugger
let navigationStartTime = 0;
const NAVIGATION_GRACE_MS = 3000;

let extensionAlive = true;
let messageQueue: Array<{type: string; payload?: unknown}> = [];
let flushTimeout: number | null = null;
Expand All @@ -24,6 +22,7 @@ declare class WeakRef<T extends object> { constructor(target: T); deref(): T | u
let currentThrottle = 100;
const MAX_BATCH_SIZE = 100;
const MAX_QUEUE_SIZE = 500;
const LEAKY_SET_TTL_MS = 5 * 60 * 1000;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The constant LEAKY_SET_TTL_MS now governs Map instances (reportedEffectIssues, reportedExcessiveRerenders, reportedSlowRenders) rather than Set instances. To improve maintainability and avoid confusion, consider renaming this constant to LEAKY_MAP_TTL_MS or REPORTED_ISSUES_TTL_MS (and updating its references in periodicCleanup).

Suggested change
const LEAKY_SET_TTL_MS = 5 * 60 * 1000;
const LEAKY_MAP_TTL_MS = 5 * 60 * 1000;


function getAdaptiveThrottle(): number {
const now = Date.now();
Expand Down Expand Up @@ -335,9 +334,9 @@ declare class WeakRef<T extends object> { constructor(target: T); deref(): T | u
const renderCounts = new Map<string, number>();
const lastRenderTimes = new Map<string, number>();
const recentRenderTimestamps = new Map<string, number[]>();
const reportedEffectIssues = new Set<string>();
const reportedExcessiveRerenders = new Set<string>();
const reportedSlowRenders = new Set<string>();
const reportedEffectIssues = new Map<string, number>();
const reportedExcessiveRerenders = new Map<string, number>();
const reportedSlowRenders = new Map<string, number>();

const EXCESSIVE_RENDER_THRESHOLD = 10;
const EXCESSIVE_RENDER_WINDOW_MS = 1000;
Expand Down Expand Up @@ -700,7 +699,7 @@ declare class WeakRef<T extends object> { constructor(target: T); deref(): T | u
if (needsCleanup && destroyFn === undefined) {
const issueKey = `${componentName}_MISSING_CLEANUP_${effectIndex}`;
if (!reportedEffectIssues.has(issueKey)) {
reportedEffectIssues.add(issueKey);
reportedEffectIssues.set(issueKey, Date.now());
let resourceType = 'resource';
if (hasTimerPattern) resourceType = 'timer (setInterval/setTimeout)';
else if (hasEventListenerPattern) resourceType = 'event listener';
Expand Down Expand Up @@ -731,7 +730,7 @@ declare class WeakRef<T extends object> { constructor(target: T); deref(): T | u
if (!hasTimerPattern && !hasEventListenerPattern) {
const issueKey = `${componentName}_INFINITE_LOOP_RISK_${effectIndex}`;
if (!reportedEffectIssues.has(issueKey)) {
reportedEffectIssues.add(issueKey);
reportedEffectIssues.set(issueKey, Date.now());
issues.push({
id: generateId(),
type: 'INFINITE_LOOP_RISK',
Expand All @@ -755,7 +754,7 @@ declare class WeakRef<T extends object> { constructor(target: T); deref(): T | u
if (usesPropsOrState) {
const issueKey = `${componentName}_MISSING_DEP_${effectIndex}`;
if (!reportedEffectIssues.has(issueKey)) {
reportedEffectIssues.add(issueKey);
reportedEffectIssues.set(issueKey, Date.now());
issues.push({
id: generateId(),
type: 'MISSING_DEP',
Expand Down Expand Up @@ -1588,7 +1587,7 @@ declare class WeakRef<T extends object> { constructor(target: T); deref(): T | u
if (rendersInLastSecond >= EXCESSIVE_RENDER_THRESHOLD) {
const issueKey = `excessive_${fiberId}`;
if (!reportedExcessiveRerenders.has(issueKey)) {
reportedExcessiveRerenders.add(issueKey);
reportedExcessiveRerenders.set(issueKey, Date.now());
}
issues.push({
id: issueKey,
Expand All @@ -1604,7 +1603,7 @@ declare class WeakRef<T extends object> { constructor(target: T); deref(): T | u

if (actualDuration > 16) {
if (!reportedSlowRenders.has(fiberId)) {
reportedSlowRenders.add(fiberId);
reportedSlowRenders.set(fiberId, Date.now());
issues.push({
id: generateId(),
type: 'SLOW_RENDER',
Expand Down Expand Up @@ -2901,10 +2900,7 @@ declare class WeakRef<T extends object> { constructor(target: T); deref(): T | u

function stopAllMonitoring(): void {
// Fix 5: Stop periodic cleanup
if ((window as any).__REACT_DEBUGGER_CLEANUP_INTERVAL__) {
clearInterval((window as any).__REACT_DEBUGGER_CLEANUP_INTERVAL__);
(window as any).__REACT_DEBUGGER_CLEANUP_INTERVAL__ = null;
}
uninstallCleanupInterval();
stopMemoryMonitoring();
toggleScan(false);
reduxSearchStopped = true;
Expand Down Expand Up @@ -2963,6 +2959,24 @@ declare class WeakRef<T extends object> { constructor(target: T); deref(): T | u
if (pathCache.size > PATH_CACHE_LIMIT) {
clearPathCache();
}

for (const [key, ts] of reportedEffectIssues) {
if (now - ts > LEAKY_SET_TTL_MS) {
reportedEffectIssues.delete(key);
}
}

for (const [key, ts] of reportedExcessiveRerenders) {
if (now - ts > LEAKY_SET_TTL_MS) {
reportedExcessiveRerenders.delete(key);
}
}

for (const [key, ts] of reportedSlowRenders) {
if (now - ts > LEAKY_SET_TTL_MS) {
reportedSlowRenders.delete(key);
}
}
}

(window as any).__REACT_DEBUGGER_MEMORY__ = {
Expand Down Expand Up @@ -3222,7 +3236,6 @@ declare class WeakRef<T extends object> { constructor(target: T); deref(): T | u
return;
}
debuggerEnabled = true;
navigationStartTime = Date.now();
// Re-send REACT_DETECTED since inject.js may have loaded after React initialized
const hook = (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__;
if (hook?.renderers?.size > 0) {
Expand All @@ -3239,9 +3252,7 @@ declare class WeakRef<T extends object> { constructor(target: T); deref(): T | u
forceReanalyze();
}, 500);
// Start periodic cleanup (inside ENABLE_DEBUGGER, not on every message)
if (!(window as any).__REACT_DEBUGGER_CLEANUP_INTERVAL__) {
(window as any).__REACT_DEBUGGER_CLEANUP_INTERVAL__ = window.setInterval(periodicCleanup, 60000);
}
installCleanupInterval(periodicCleanup);
sendFromPage('DEBUGGER_STATE_CHANGED', { enabled: true });
log('Debugger enabled');
}
Expand Down
89 changes: 89 additions & 0 deletions src/inject/lifecycle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* src/inject/lifecycle.ts
*
* Leaf module that owns the install/uninstall of named lifecycle intervals
* for the React Debugger inject script.
*
* Design constraints (M-A consolidation):
* - This file MUST NOT import anything from src/inject/index.ts (no circular
* dependency). It is a pure leaf: it accepts callbacks/values as parameters
* and manipulates the window namespace directly.
* - Callers pass callbacks (e.g. periodicCleanup) rather than this module
* closing over inject internals.
* - The (window as any) casts are intentional: they follow the existing
* host-page-window-namespace pattern used throughout inject/index.ts.
*
* M-A scope (T5): cleanup-interval pair only.
* Memory monitor + closure tracking extractions are deferred to M-B, where
* a full install(flags)/uninstall()/toggle() API will be introduced together
* with a hook registry.
*/

/**
* Default interval duration for the periodic cleanup timer (ms).
* Matches the hardcoded 60000 previously inlined at inject/index.ts:3262.
*/
const DEFAULT_CLEANUP_INTERVAL_MS = 60_000;

/**
* Install the periodic cleanup interval on the host-page window.
*
* Consolidates the pattern previously inlined at inject/index.ts lines 3261-3263
* (inside the ENABLE_DEBUGGER message handler):
*
* ```ts
* if (!(window as any).__REACT_DEBUGGER_CLEANUP_INTERVAL__) {
* (window as any).__REACT_DEBUGGER_CLEANUP_INTERVAL__ =
* window.setInterval(periodicCleanup, 60000);
* }
* ```
*
* The window-namespace guard prevents double-install (idempotent). The window
* key is set to the interval ID so that uninstallCleanupInterval() can clear it.
*
* Future intent (M-B): this function will become part of lifecycle.install(flags)
* and will be driven by the hook registry rather than called ad-hoc.
*
* @param periodicCleanup - The cleanup callback to run on each interval tick.
* Defined and closed-over inside inject/index.ts; passed here as a parameter
* to keep this module free of inject internals.
* @param intervalMs - Interval duration in milliseconds.
* Defaults to DEFAULT_CLEANUP_INTERVAL_MS (60 000).
*/
export function installCleanupInterval(
periodicCleanup: () => void,
intervalMs: number = DEFAULT_CLEANUP_INTERVAL_MS,
): void {
if (!(window as any).__REACT_DEBUGGER_CLEANUP_INTERVAL__) {
(window as any).__REACT_DEBUGGER_CLEANUP_INTERVAL__ = window.setInterval(
periodicCleanup,
intervalMs,
);
}
}

/**
* Clear the periodic cleanup interval and remove the window-namespace key.
*
* Consolidates the pattern previously inlined at inject/index.ts lines 2905-2908
* (inside stopAllMonitoring()):
*
* ```ts
* if ((window as any).__REACT_DEBUGGER_CLEANUP_INTERVAL__) {
* clearInterval((window as any).__REACT_DEBUGGER_CLEANUP_INTERVAL__);
* (window as any).__REACT_DEBUGGER_CLEANUP_INTERVAL__ = null;
* }
* ```
*
* Safe to call multiple times (idempotent): the guard skips the clearInterval
* call if the key is already null/undefined.
*
* Future intent (M-B): will become part of lifecycle.uninstall() driven by the
* hook registry, with reverse-priority ordering.
*/
export function uninstallCleanupInterval(): void {
if ((window as any).__REACT_DEBUGGER_CLEANUP_INTERVAL__) {
clearInterval((window as any).__REACT_DEBUGGER_CLEANUP_INTERVAL__);
(window as any).__REACT_DEBUGGER_CLEANUP_INTERVAL__ = null;
}
}
4 changes: 2 additions & 2 deletions src/panel/Panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -331,9 +331,9 @@ export function Panel() {
const getBadge = (tabId: TabId): number | undefined => {
switch (tabId) {
case 'ui-state':
return getIssueCount(['DIRECT_STATE_MUTATION', 'MISSING_KEY', 'INDEX_AS_KEY', 'DUPLICATE_KEY']) || undefined;
return getIssueCount(['MISSING_KEY', 'INDEX_AS_KEY']) || undefined;
case 'performance':
return getIssueCount(['EXCESSIVE_RERENDERS', 'UNNECESSARY_RERENDER']) || undefined;
return getIssueCount(['EXCESSIVE_RERENDERS']) || undefined;
case 'side-effects':
return getIssueCount(['MISSING_CLEANUP', 'MISSING_DEP', 'INFINITE_LOOP_RISK']) || undefined;
case 'cls':
Expand Down
25 changes: 0 additions & 25 deletions src/panel/components/IssueCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,6 @@ const SEVERITY_CONFIG = {
};

const ISSUE_INFO: Record<string, { title: string; why: string; learnUrl?: string }> = {
DIRECT_STATE_MUTATION: {
title: 'Direct State Mutation',
why: 'React cannot detect direct state mutations and will not re-render the component.',
learnUrl: 'https://react.dev/learn/updating-objects-in-state',
},
MISSING_KEY: {
title: 'Missing Key in List',
why: 'Keys help React identify which items have changed, are added, or removed.',
Expand All @@ -27,11 +22,6 @@ const ISSUE_INFO: Record<string, { title: string; why: string; learnUrl?: string
why: 'Using index as key can cause issues when list items are reordered or filtered.',
learnUrl: 'https://react.dev/learn/rendering-lists#why-does-react-need-keys',
},
DUPLICATE_KEY: {
title: 'Duplicate Keys',
why: 'Duplicate keys will cause React to incorrectly update and render components.',
learnUrl: 'https://react.dev/learn/rendering-lists',
},
MISSING_CLEANUP: {
title: 'Missing Effect Cleanup',
why: 'Effects with subscriptions, timers, or event listeners need cleanup to prevent memory leaks.',
Expand All @@ -42,11 +32,6 @@ const ISSUE_INFO: Record<string, { title: string; why: string; learnUrl?: string
why: 'Missing dependencies can cause stale closures and bugs.',
learnUrl: 'https://react.dev/reference/react/useEffect#specifying-reactive-dependencies',
},
EXTRA_DEP: {
title: 'Unnecessary Dependencies',
why: 'Extra dependencies can cause effects to run more often than needed.',
learnUrl: 'https://react.dev/reference/react/useEffect#removing-unnecessary-dependencies',
},
INFINITE_LOOP_RISK: {
title: 'Infinite Loop Risk',
why: 'This effect may cause infinite re-renders if state is updated without proper guards.',
Expand All @@ -57,16 +42,6 @@ const ISSUE_INFO: Record<string, { title: string; why: string; learnUrl?: string
why: 'Too many re-renders can cause performance issues and poor user experience.',
learnUrl: 'https://react.dev/reference/react/memo',
},
UNNECESSARY_RERENDER: {
title: 'Unnecessary Re-render',
why: 'Component re-rendered when its props and state did not change.',
learnUrl: 'https://react.dev/reference/react/useMemo',
},
DEV_MODE_IN_PROD: {
title: 'Development Mode in Production',
why: 'Running React in development mode significantly impacts performance.',
learnUrl: 'https://react.dev/learn/react-developer-tools#development-vs-production-builds',
},
STALE_CLOSURE: {
title: 'Stale Closure Detected',
why: 'This callback was created in an earlier render and may be using outdated state or props values. This is one of the most common and hard-to-debug React bugs.',
Expand Down
Loading
Loading