[DevTools] Remove FlowFixMe from extension lifecycle (#37152)
I've been triaging some lifecycle races during sessions with BFCache involved and noticed a few type errors in real world scenario. These objects were not properly typed, fixing in this PR.
Ruslan Lesiutin committed
Jul 30, 2026 at 21:17 UTC
c56a44903f91d41dce51107f34491daabdcb6a70
1 file changed
+74
-59
packages/react-devtools-extensions/src/main/index.js
+74
-59
@@ -67,6 +67,13 @@ type PendingBridgeMessage = {
67
transferable?: $ReadOnlyArray<mixed>,
68
};
69
70
+type DevToolsInstance = {
71
+ bridge: FrontendBridge,
72
+ store: Store,
73
+ render: (overrideTab?: TabID) => void,
74
+ root: RootType,
75
+};
76
+
77
function flushPendingBridgeMessages(): void {
78
const currentPort = port;
79
if (!isBridgeConnected || currentPort === null) {
@@ -128,8 +135,8 @@ function addBridgePortListener(nextPort: ExtensionRuntimePort): void {
135
bridgePortListener = nextBridgePortListener;
136
}
137
131
-function createBridge() {
132
- bridge = new Bridge({
138
+function createBridge(): FrontendBridge {
139
+ const bridge: FrontendBridge = new Bridge({
140
listen(fn) {
141
const currentPort = port;
142
if (currentPort === null) {
@@ -199,7 +206,13 @@ function createBridge() {
206
},
207
};
208
// Rerender with the new file selection.
202
- render();
209
+ const instance = devToolsInstance;
210
+ if (instance === null) {
211
+ throw new Error(
212
+ 'Cannot sync source selection: DevTools instance is not initialized.',
213
+ );
214
+ }
215
+ instance.render();
216
} else {
217
// Update the ref to the latest position without updating the url. No need to rerender.
218
const selectionRef = currentSelectedSource.selectionRef;
@@ -229,14 +242,16 @@ function createBridge() {
242
onBrowserSourceSelectionChanged,
243
);
244
}
245
+
246
+ return bridge;
247
}
248
234
-function createBridgeAndStore() {
235
- createBridge();
249
+function createDevToolsInstance(): DevToolsInstance {
250
+ const bridge = createBridge();
251
252
const {isProfiling} = getProfilingFlags();
253
239
- store = new Store(bridge, {
254
+ const store = new Store(bridge, {
255
isProfiling,
256
supportsReloadAndProfile: __IS_CHROME__ || __IS_EDGE__,
257
// At this time, the timeline can only parse Chrome performance profiles.
@@ -285,9 +300,9 @@ function createBridgeAndStore() {
300
);
301
};
302
288
- root = createRoot(document.createElement('div'));
303
+ const root = createRoot(document.createElement('div'));
304
290
- render = (overrideTab: TabID | null = mostRecentOverrideTab) => {
305
+ const render = (overrideTab: TabID | null = mostRecentOverrideTab) => {
306
mostRecentOverrideTab = overrideTab;
307
308
root.render(
@@ -314,6 +329,8 @@ function createBridgeAndStore() {
329
}),
330
);
331
};
332
+
333
+ return {bridge, store, render, root};
334
}
335
336
function ensureInitialHTMLIsCleared(
@@ -327,11 +344,11 @@ function ensureInitialHTMLIsCleared(
344
container._hasInitialHTMLBeenCleared = true;
345
}
346
330
-function createComponentsPanel() {
347
+function createComponentsPanel(instance: DevToolsInstance) {
348
if (componentsPortalContainer) {
349
// Panel is created and user opened it at least once
350
ensureInitialHTMLIsCleared(componentsPortalContainer);
334
- render('components');
351
+ instance.render('components');
352
353
return;
354
}
@@ -350,10 +367,11 @@ function createComponentsPanel() {
367
368
createdPanel.onShown.addListener(portal => {
369
componentsPortalContainer = portal.container;
353
- if (componentsPortalContainer != null && render) {
370
+ const currentInstance = devToolsInstance;
371
+ if (componentsPortalContainer != null && currentInstance !== null) {
372
ensureInitialHTMLIsCleared(componentsPortalContainer);
373
356
- render('components');
374
+ currentInstance.render('components');
375
portal.injectStyles(cloneStyleTags);
376
377
logEvent({event_name: 'selected-components-tab'});
@@ -361,20 +379,20 @@ function createComponentsPanel() {
379
});
380
381
createdPanel.onShown.addListener(() => {
364
- bridge.emit('extensionComponentsPanelShown');
382
+ devToolsInstance?.bridge.emit('extensionComponentsPanelShown');
383
});
384
createdPanel.onHidden.addListener(() => {
367
- bridge.emit('extensionComponentsPanelHidden');
385
+ devToolsInstance?.bridge.emit('extensionComponentsPanelHidden');
386
});
387
},
388
);
389
}
390
373
-function createElementsInspectPanel() {
391
+function createElementsInspectPanel(instance: DevToolsInstance) {
392
if (inspectedElementPortalContainer) {
393
// Panel is created and user opened it at least once
394
ensureInitialHTMLIsCleared(inspectedElementPortalContainer);
377
- render();
395
+ instance.render();
396
397
return;
398
}
@@ -403,11 +421,12 @@ function createElementsInspectPanel() {
421
422
createdPane.onShown.addListener(portal => {
423
inspectedElementPortalContainer = portal.container;
406
- if (inspectedElementPortalContainer != null && render) {
424
+ const currentInstance = devToolsInstance;
425
+ if (inspectedElementPortalContainer != null && currentInstance !== null) {
426
ensureInitialHTMLIsCleared(inspectedElementPortalContainer);
408
- bridge.send('syncSelectionFromBuiltinElementsPanel');
427
+ currentInstance.bridge.send('syncSelectionFromBuiltinElementsPanel');
428
410
- render();
429
+ currentInstance.render();
430
portal.injectStyles(cloneStyleTags);
431
432
logEvent({event_name: 'selected-inspected-element-pane'});
@@ -416,11 +435,11 @@ function createElementsInspectPanel() {
435
});
436
}
437
419
-function createProfilerPanel() {
438
+function createProfilerPanel(instance: DevToolsInstance) {
439
if (profilerPortalContainer) {
440
// Panel is created and user opened it at least once
441
ensureInitialHTMLIsCleared(profilerPortalContainer);
423
- render('profiler');
442
+ instance.render('profiler');
443
444
return;
445
}
@@ -439,10 +458,11 @@ function createProfilerPanel() {
458
459
createdPanel.onShown.addListener(portal => {
460
profilerPortalContainer = portal.container;
442
- if (profilerPortalContainer != null && render) {
461
+ const currentInstance = devToolsInstance;
462
+ if (profilerPortalContainer != null && currentInstance !== null) {
463
ensureInitialHTMLIsCleared(profilerPortalContainer);
464
445
- render('profiler');
465
+ currentInstance.render('profiler');
466
portal.injectStyles(cloneStyleTags);
467
468
logEvent({event_name: 'selected-profiler-tab'});
@@ -452,11 +472,11 @@ function createProfilerPanel() {
472
);
473
}
474
455
-function createSourcesEditorPanel() {
475
+function createSourcesEditorPanel(instance: DevToolsInstance) {
476
if (editorPortalContainer) {
477
// Panel is created and user opened it at least once
478
ensureInitialHTMLIsCleared(editorPortalContainer);
459
- render();
479
+ instance.render();
480
481
return;
482
}
@@ -480,10 +500,11 @@ function createSourcesEditorPanel() {
500
501
createdPane.onShown.addListener(portal => {
502
editorPortalContainer = portal.container;
483
- if (editorPortalContainer != null && render) {
503
+ const currentInstance = devToolsInstance;
504
+ if (editorPortalContainer != null && currentInstance !== null) {
505
ensureInitialHTMLIsCleared(editorPortalContainer);
506
486
- render();
507
+ currentInstance.render();
508
portal.injectStyles(cloneStyleTags);
509
510
logEvent({event_name: 'selected-editor-pane'});
@@ -492,11 +513,11 @@ function createSourcesEditorPanel() {
513
});
514
}
515
495
-function createSuspensePanel() {
516
+function createSuspensePanel(instance: DevToolsInstance) {
517
if (suspensePortalContainer) {
518
// Panel is created and user opened it at least once
519
ensureInitialHTMLIsCleared(suspensePortalContainer);
499
- render('suspense');
520
+ instance.render('suspense');
521
522
return;
523
}
@@ -515,10 +536,11 @@ function createSuspensePanel() {
536
537
createdPanel.onShown.addListener(portal => {
538
suspensePortalContainer = portal.container;
518
- if (suspensePortalContainer != null && render) {
539
+ const currentInstance = devToolsInstance;
540
+ if (suspensePortalContainer != null && currentInstance !== null) {
541
ensureInitialHTMLIsCleared(suspensePortalContainer);
542
521
- render('suspense');
543
+ currentInstance.render('suspense');
544
portal.injectStyles(cloneStyleTags);
545
546
logEvent({event_name: 'selected-suspense-tab'});
@@ -532,10 +554,10 @@ function performInTabNavigationCleanup() {
554
// Potentially, if react hasn't loaded yet and user performs in-tab navigation
555
clearReactPollingInstance();
556
535
- // $FlowFixMe[invalid-compare]
536
- if (store !== null) {
557
+ const instance = devToolsInstance;
558
+ if (instance !== null) {
559
// Store profiling data, so it can be used later
538
- profilingData = store.profilerStore.profilingData;
560
+ profilingData = instance.store.profilerStore.profilingData;
561
}
562
563
// If panels were already created, and we have already mounted React root to display
@@ -544,17 +566,17 @@ function performInTabNavigationCleanup() {
566
(componentsPortalContainer ||
567
profilerPortalContainer ||
568
suspensePortalContainer) &&
547
- root
569
+ instance !== null
570
) {
571
// It's easiest to recreate the DevTools panel (to clean up potential stale state).
572
// We can revisit this in the future as a small optimization.
573
// This should also emit bridge.shutdown, but only if this root was mounted
552
- flushSync(() => root.unmount());
574
+ flushSync(() => instance.root.unmount());
575
} else {
576
// In case Browser DevTools were opened, but user never pressed on extension panels
577
// They were never mounted and there is nothing to unmount, but we need to emit shutdown event
578
// because bridge was already created
557
- bridge?.shutdown();
579
+ instance?.bridge.shutdown();
580
}
581
582
// Do not nullify componentsPanelPortal and profilerPanelPortal on purpose,
@@ -565,10 +587,7 @@ function performInTabNavigationCleanup() {
587
// Do not clean mostRecentOverrideTab on purpose, so we remember last opened
588
// React DevTools tab, when user does in-tab navigation
589
568
- store = null as $FlowFixMe;
569
- bridge = null as $FlowFixMe;
570
- render = null as $FlowFixMe;
571
- root = null as $FlowFixMe;
590
+ devToolsInstance = null;
591
pendingBridgeMessages.length = 0;
592
}
593
@@ -576,27 +595,25 @@ function performFullCleanup() {
595
// Potentially, if react hasn't loaded yet and user closed the browser DevTools
596
clearReactPollingInstance();
597
598
+ const instance = devToolsInstance;
599
if (
600
(componentsPortalContainer ||
601
profilerPortalContainer ||
602
suspensePortalContainer) &&
583
- root
603
+ instance !== null
604
) {
605
// This should also emit bridge.shutdown, but only if this root was mounted
586
- flushSync(() => root.unmount());
606
+ flushSync(() => instance.root.unmount());
607
} else {
588
- bridge?.shutdown();
608
+ instance?.bridge.shutdown();
609
}
610
611
componentsPortalContainer = null;
612
profilerPortalContainer = null;
613
suspensePortalContainer = null;
594
- root = null as $FlowFixMe;
614
615
mostRecentOverrideTab = null;
597
- store = null as $FlowFixMe;
598
- bridge = null as $FlowFixMe;
599
- render = null as $FlowFixMe;
616
+ devToolsInstance = null;
617
pendingBridgeMessages.length = 0;
618
619
port?.disconnect();
@@ -646,13 +663,14 @@ function mountReactDevTools() {
663
664
registerEventsLogger();
665
649
- createBridgeAndStore();
666
+ const instance = createDevToolsInstance();
667
+ devToolsInstance = instance;
668
651
- createComponentsPanel();
652
- createProfilerPanel();
653
- createSourcesEditorPanel();
654
- createElementsInspectPanel();
655
- createSuspensePanel();
669
+ createComponentsPanel(instance);
670
+ createProfilerPanel(instance);
671
+ createSourcesEditorPanel(instance);
672
+ createElementsInspectPanel(instance);
673
+ createSuspensePanel(instance);
674
}
675
676
let reactPollingInstance = null;
@@ -689,11 +707,10 @@ function mountReactDevToolsWhenReactHasLoaded() {
707
);
708
}
709
692
-let bridge: FrontendBridge = null as $FlowFixMe;
710
+let devToolsInstance: DevToolsInstance | null = null;
711
let lastSubscribedBridgeListener: ((message: mixed) => void) | null = null;
712
let subscribedBridgePort: ExtensionRuntimePort | null = null;
713
let bridgePortListener: ((message: mixed) => void) | null = null;
696
-let store: Store = null as $FlowFixMe;
714
715
let profilingData = null;
716
@@ -709,8 +726,6 @@ let editorPortalContainer = null;
726
let inspectedElementPortalContainer = null;
727
728
let mostRecentOverrideTab: null | TabID = null;
712
-let render: (overrideTab?: TabID) => void = null as $FlowFixMe;
713
-let root: RootType = null as $FlowFixMe;
729
730
let currentSelectedSource: null | SourceSelection = null;
731
@@ -751,7 +766,7 @@ mountReactDevToolsWhenReactHasLoaded();
766
767
function onThemeChanged() {
768
// Rerender with the new theme
754
- render();
769
+ devToolsInstance?.render();
770
}
771
772
if (chrome.devtools.panels.setThemeChangeHandler) {