@samitouri / QOS-React-1 / commits / c56a44903f

[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) {