[DevTools] Create extension panels before React detection (#37151)
The main reason why I am doing this is because there is no API in the browser to "unmount" the created panel. If you have React DevTools installed, we should always create a panel, but the contents of the panel should be dynamically populated based on the target. If it is not a React app, we will continue showing the stub message. Previously, we wouldn't mount a panel at all, and historically we've received a few reports of this as a bug.
Ruslan Lesiutin committed
Jul 30, 2026 at 21:17 UTC
7acf00566e35c40817342654438a5757c6c41d0e
1 file changed
+81
-77
packages/react-devtools-extensions/src/main/index.js
+81
-77
@@ -344,15 +344,7 @@ function ensureInitialHTMLIsCleared(
344
container._hasInitialHTMLBeenCleared = true;
345
}
346
347
-function createComponentsPanel(instance: DevToolsInstance) {
348
- if (componentsPortalContainer) {
349
- // Panel is created and user opened it at least once
350
- ensureInitialHTMLIsCleared(componentsPortalContainer);
351
- instance.render('components');
352
-
353
- return;
354
- }
355
-
347
+function createComponentsPanel() {
348
if (componentsPanel) {
349
// Panel is created, but wasn't opened yet, so no document is present for it
350
return;
@@ -368,13 +360,15 @@ function createComponentsPanel(instance: DevToolsInstance) {
360
createdPanel.onShown.addListener(portal => {
361
componentsPortalContainer = portal.container;
362
const currentInstance = devToolsInstance;
371
- if (componentsPortalContainer != null && currentInstance !== null) {
372
- ensureInitialHTMLIsCleared(componentsPortalContainer);
363
+ if (componentsPortalContainer != null) {
364
+ if (currentInstance !== null) {
365
+ ensureInitialHTMLIsCleared(componentsPortalContainer);
366
374
- currentInstance.render('components');
375
- portal.injectStyles(cloneStyleTags);
367
+ currentInstance.render('components');
368
377
- logEvent({event_name: 'selected-components-tab'});
369
+ logEvent({event_name: 'selected-components-tab'});
370
+ }
371
+ portal.injectStyles(cloneStyleTags);
372
}
373
});
374
@@ -388,15 +382,7 @@ function createComponentsPanel(instance: DevToolsInstance) {
382
);
383
}
384
391
-function createElementsInspectPanel(instance: DevToolsInstance) {
392
- if (inspectedElementPortalContainer) {
393
- // Panel is created and user opened it at least once
394
- ensureInitialHTMLIsCleared(inspectedElementPortalContainer);
395
- instance.render();
396
-
397
- return;
398
- }
399
-
385
+function createElementsInspectPanel() {
386
if (inspectedElementPane) {
387
// Panel is created, but wasn't opened yet, so no document is present for it
388
return;
@@ -422,28 +408,22 @@ function createElementsInspectPanel(instance: DevToolsInstance) {
408
createdPane.onShown.addListener(portal => {
409
inspectedElementPortalContainer = portal.container;
410
const currentInstance = devToolsInstance;
425
- if (inspectedElementPortalContainer != null && currentInstance !== null) {
426
- ensureInitialHTMLIsCleared(inspectedElementPortalContainer);
427
- currentInstance.bridge.send('syncSelectionFromBuiltinElementsPanel');
411
+ if (inspectedElementPortalContainer != null) {
412
+ if (currentInstance !== null) {
413
+ ensureInitialHTMLIsCleared(inspectedElementPortalContainer);
414
+ currentInstance.bridge.send('syncSelectionFromBuiltinElementsPanel');
415
429
- currentInstance.render();
430
- portal.injectStyles(cloneStyleTags);
416
+ currentInstance.render();
417
432
- logEvent({event_name: 'selected-inspected-element-pane'});
418
+ logEvent({event_name: 'selected-inspected-element-pane'});
419
+ }
420
+ portal.injectStyles(cloneStyleTags);
421
}
422
});
423
});
424
}
425
438
-function createProfilerPanel(instance: DevToolsInstance) {
439
- if (profilerPortalContainer) {
440
- // Panel is created and user opened it at least once
441
- ensureInitialHTMLIsCleared(profilerPortalContainer);
442
- instance.render('profiler');
443
-
444
- return;
445
- }
446
-
426
+function createProfilerPanel() {
427
if (profilerPanel) {
428
// Panel is created, but wasn't opened yet, so no document is present for it
429
return;
@@ -459,28 +439,22 @@ function createProfilerPanel(instance: DevToolsInstance) {
439
createdPanel.onShown.addListener(portal => {
440
profilerPortalContainer = portal.container;
441
const currentInstance = devToolsInstance;
462
- if (profilerPortalContainer != null && currentInstance !== null) {
463
- ensureInitialHTMLIsCleared(profilerPortalContainer);
442
+ if (profilerPortalContainer != null) {
443
+ if (currentInstance !== null) {
444
+ ensureInitialHTMLIsCleared(profilerPortalContainer);
445
465
- currentInstance.render('profiler');
466
- portal.injectStyles(cloneStyleTags);
446
+ currentInstance.render('profiler');
447
468
- logEvent({event_name: 'selected-profiler-tab'});
448
+ logEvent({event_name: 'selected-profiler-tab'});
449
+ }
450
+ portal.injectStyles(cloneStyleTags);
451
}
452
});
453
},
454
);
455
}
456
475
-function createSourcesEditorPanel(instance: DevToolsInstance) {
476
- if (editorPortalContainer) {
477
- // Panel is created and user opened it at least once
478
- ensureInitialHTMLIsCleared(editorPortalContainer);
479
- instance.render();
480
-
481
- return;
482
- }
483
-
457
+function createSourcesEditorPanel() {
458
if (editorPane) {
459
// Panel is created, but wasn't opened yet, so no document is present for it
460
return;
@@ -501,27 +475,21 @@ function createSourcesEditorPanel(instance: DevToolsInstance) {
475
createdPane.onShown.addListener(portal => {
476
editorPortalContainer = portal.container;
477
const currentInstance = devToolsInstance;
504
- if (editorPortalContainer != null && currentInstance !== null) {
505
- ensureInitialHTMLIsCleared(editorPortalContainer);
478
+ if (editorPortalContainer != null) {
479
+ if (currentInstance !== null) {
480
+ ensureInitialHTMLIsCleared(editorPortalContainer);
481
507
- currentInstance.render();
508
- portal.injectStyles(cloneStyleTags);
482
+ currentInstance.render();
483
510
- logEvent({event_name: 'selected-editor-pane'});
484
+ logEvent({event_name: 'selected-editor-pane'});
485
+ }
486
+ portal.injectStyles(cloneStyleTags);
487
}
488
});
489
});
490
}
491
516
-function createSuspensePanel(instance: DevToolsInstance) {
517
- if (suspensePortalContainer) {
518
- // Panel is created and user opened it at least once
519
- ensureInitialHTMLIsCleared(suspensePortalContainer);
520
- instance.render('suspense');
521
-
522
- return;
523
- }
524
-
492
+function createSuspensePanel() {
493
if (suspensePanel) {
494
// Panel is created, but wasn't opened yet, so no document is present for it
495
return;
@@ -537,19 +505,57 @@ function createSuspensePanel(instance: DevToolsInstance) {
505
createdPanel.onShown.addListener(portal => {
506
suspensePortalContainer = portal.container;
507
const currentInstance = devToolsInstance;
540
- if (suspensePortalContainer != null && currentInstance !== null) {
541
- ensureInitialHTMLIsCleared(suspensePortalContainer);
508
+ if (suspensePortalContainer != null) {
509
+ if (currentInstance !== null) {
510
+ ensureInitialHTMLIsCleared(suspensePortalContainer);
511
543
- currentInstance.render('suspense');
544
- portal.injectStyles(cloneStyleTags);
512
+ currentInstance.render('suspense');
513
546
- logEvent({event_name: 'selected-suspense-tab'});
514
+ logEvent({event_name: 'selected-suspense-tab'});
515
+ }
516
+ portal.injectStyles(cloneStyleTags);
517
}
518
});
519
},
520
);
521
}
522
523
+function createDevToolsPanels(): void {
524
+ createComponentsPanel();
525
+ createProfilerPanel();
526
+ createSourcesEditorPanel();
527
+ createElementsInspectPanel();
528
+ createSuspensePanel();
529
+}
530
+
531
+function renderOpenedDevToolsPanels(instance: DevToolsInstance): void {
532
+ if (componentsPortalContainer) {
533
+ ensureInitialHTMLIsCleared(componentsPortalContainer);
534
+ instance.render('components');
535
+ }
536
+
537
+ if (profilerPortalContainer) {
538
+ ensureInitialHTMLIsCleared(profilerPortalContainer);
539
+ instance.render('profiler');
540
+ }
541
+
542
+ if (editorPortalContainer) {
543
+ ensureInitialHTMLIsCleared(editorPortalContainer);
544
+ instance.render();
545
+ }
546
+
547
+ if (inspectedElementPortalContainer) {
548
+ ensureInitialHTMLIsCleared(inspectedElementPortalContainer);
549
+ instance.bridge.send('syncSelectionFromBuiltinElementsPanel');
550
+ instance.render();
551
+ }
552
+
553
+ if (suspensePortalContainer) {
554
+ ensureInitialHTMLIsCleared(suspensePortalContainer);
555
+ instance.render('suspense');
556
+ }
557
+}
558
+
559
function performInTabNavigationCleanup() {
560
// Potentially, if react hasn't loaded yet and user performs in-tab navigation
561
clearReactPollingInstance();
@@ -666,11 +672,7 @@ function mountReactDevTools() {
672
const instance = createDevToolsInstance();
673
devToolsInstance = instance;
674
669
- createComponentsPanel(instance);
670
- createProfilerPanel(instance);
671
- createSourcesEditorPanel(instance);
672
- createElementsInspectPanel(instance);
673
- createSuspensePanel(instance);
675
+ renderOpenedDevToolsPanels(instance);
676
}
677
678
let reactPollingInstance = null;
@@ -743,7 +745,7 @@ const debouncedMountReactDevToolsCallback = debounce(
745
500,
746
);
747
746
-// Clean up everything, but start mounting React DevTools panels if user stays at this page
748
+// Clean up everything, but remount React DevTools if user stays at this page
749
function onNavigatedToOtherPage() {
750
performInTabNavigationCleanup();
751
debouncedMountReactDevToolsCallback();
@@ -762,6 +764,8 @@ if (__IS_FIREFOX__) {
764
765
connectExtensionPort();
766
767
+createDevToolsPanels();
768
+
769
mountReactDevToolsWhenReactHasLoaded();
770
771
function onThemeChanged() {