Skip to content

Commit be7e5bc

Browse files
committed
feat[devtools/extension]: show disclaimer when page doesnt run react and refactor react polling logic
1 parent 54c2f2a commit be7e5bc

File tree

4 files changed

+158
-68
lines changed

4 files changed

+158
-68
lines changed

packages/react-devtools-extensions/panel.html

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,29 @@
2222
right: 0;
2323
bottom: 0;
2424
}
25+
.no-react-disclaimer {
26+
margin: 16px;
27+
font-family: Courier, monospace, serif;
28+
font-size: 16px;
29+
30+
animation: fadeIn .5s ease-in-out forwards;
31+
}
32+
33+
@keyframes fadeIn {
34+
0% {
35+
opacity: 0;
36+
}
37+
100% {
38+
opacity: 1;
39+
}
40+
}
2541
</style>
2642
</head>
2743
<body>
2844
<!-- main react mount point -->
29-
<div id="container">Unable to find React on the page.</div>
45+
<div id="container">
46+
<h1 class="no-react-disclaimer">Looks like this page doesn't have React, or it hasn't been loaded yet.</h1>
47+
</div>
3048
<script src="./build/panel.js"></script>
3149
</body>
3250
</html>

packages/react-devtools-extensions/src/background/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ chrome.runtime.onConnect.addListener(port => {
6464
const tabId = port.sender.tab.id;
6565

6666
if (ports[tabId]?.proxy) {
67-
port.disconnect();
68-
return;
67+
ports[tabId].disconnectPipe?.();
68+
ports[tabId].proxy.disconnect();
6969
}
7070

7171
registerTab(tabId);

packages/react-devtools-extensions/src/main/index.js

Lines changed: 34 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
setBrowserSelectionFromReact,
2323
setReactSelectionFromBrowser,
2424
} from './elementSelection';
25+
import {startReactPolling} from './reactPolling';
2526
import cloneStyleTags from './cloneStyleTags';
2627
import injectBackendManager from './injectBackendManager';
2728
import syncSavedPreferences from './syncSavedPreferences';
@@ -30,60 +31,6 @@ import getProfilingFlags from './getProfilingFlags';
3031
import debounce from './debounce';
3132
import './requestAnimationFramePolyfill';
3233

33-
// Try polling for at least 5 seconds, in case if it takes too long to load react
34-
const REACT_POLLING_TICK_COOLDOWN = 250;
35-
const REACT_POLLING_ATTEMPTS_THRESHOLD = 20;
36-
37-
let reactPollingTimeoutId = null;
38-
export function clearReactPollingTimeout() {
39-
clearTimeout(reactPollingTimeoutId);
40-
reactPollingTimeoutId = null;
41-
}
42-
43-
export function executeIfReactHasLoaded(callback, attempt = 1) {
44-
clearReactPollingTimeout();
45-
46-
if (attempt > REACT_POLLING_ATTEMPTS_THRESHOLD) {
47-
return;
48-
}
49-
50-
chrome.devtools.inspectedWindow.eval(
51-
'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0',
52-
(pageHasReact, exceptionInfo) => {
53-
if (exceptionInfo) {
54-
const {code, description, isError, isException, value} = exceptionInfo;
55-
56-
if (isException) {
57-
console.error(
58-
`Received error while checking if react has loaded: ${value}`,
59-
);
60-
return;
61-
}
62-
63-
if (isError) {
64-
console.error(
65-
`Received error with code ${code} while checking if react has loaded: ${description}`,
66-
);
67-
return;
68-
}
69-
}
70-
71-
if (pageHasReact) {
72-
callback();
73-
} else {
74-
reactPollingTimeoutId = setTimeout(
75-
executeIfReactHasLoaded,
76-
REACT_POLLING_TICK_COOLDOWN,
77-
callback,
78-
attempt + 1,
79-
);
80-
}
81-
},
82-
);
83-
}
84-
85-
let lastSubscribedBridgeListener = null;
86-
8734
function createBridge() {
8835
bridge = new Bridge({
8936
listen(fn) {
@@ -370,6 +317,7 @@ function ensureInitialHTMLIsCleared(container) {
370317
function createComponentsPanel() {
371318
if (componentsPortalContainer) {
372319
// Panel is created and user opened it at least once
320+
ensureInitialHTMLIsCleared(componentsPortalContainer);
373321
render('components');
374322

375323
return;
@@ -389,7 +337,7 @@ function createComponentsPanel() {
389337

390338
createdPanel.onShown.addListener(portal => {
391339
componentsPortalContainer = portal.container;
392-
if (componentsPortalContainer != null) {
340+
if (componentsPortalContainer != null && render) {
393341
ensureInitialHTMLIsCleared(componentsPortalContainer);
394342

395343
render('components');
@@ -408,6 +356,7 @@ function createComponentsPanel() {
408356
function createProfilerPanel() {
409357
if (profilerPortalContainer) {
410358
// Panel is created and user opened it at least once
359+
ensureInitialHTMLIsCleared(profilerPortalContainer);
411360
render('profiler');
412361

413362
return;
@@ -427,7 +376,7 @@ function createProfilerPanel() {
427376

428377
createdPanel.onShown.addListener(portal => {
429378
profilerPortalContainer = portal.container;
430-
if (profilerPortalContainer != null) {
379+
if (profilerPortalContainer != null && render) {
431380
ensureInitialHTMLIsCleared(profilerPortalContainer);
432381

433382
render('profiler');
@@ -442,7 +391,7 @@ function createProfilerPanel() {
442391

443392
function performInTabNavigationCleanup() {
444393
// Potentially, if react hasn't loaded yet and user performs in-tab navigation
445-
clearReactPollingTimeout();
394+
clearReactPollingInstance();
446395

447396
if (store !== null) {
448397
// Store profiling data, so it can be used later
@@ -479,7 +428,7 @@ function performInTabNavigationCleanup() {
479428

480429
function performFullCleanup() {
481430
// Potentially, if react hasn't loaded yet and user closed the browser DevTools
482-
clearReactPollingTimeout();
431+
clearReactPollingInstance();
483432

484433
if ((componentsPortalContainer || profilerPortalContainer) && root) {
485434
// This should also emit bridge.shutdown, but only if this root was mounted
@@ -531,6 +480,8 @@ function connectExtensionPort() {
531480
}
532481

533482
function mountReactDevTools() {
483+
reactPollingInstance = null;
484+
534485
registerEventsLogger();
535486

536487
createBridgeAndStore();
@@ -541,18 +492,36 @@ function mountReactDevTools() {
541492
createProfilerPanel();
542493
}
543494

544-
// TODO: display some disclaimer if user performs in-tab navigation to non-react application
545-
// when React DevTools panels are already opened, currently we will display just blank white block
546-
function mountReactDevToolsWhenReactHasLoaded() {
547-
function onReactReady() {
548-
clearReactPollingTimeout();
549-
mountReactDevTools();
495+
let reactPollingInstance = null;
496+
function clearReactPollingInstance() {
497+
reactPollingInstance?.abort();
498+
reactPollingInstance = null;
499+
}
500+
501+
function showNoReactDisclaimer() {
502+
if (componentsPortalContainer) {
503+
componentsPortalContainer.innerHTML =
504+
'<h1 class="no-react-disclaimer">Looks like this page doesn\'t have React, or it hasn\'t been loaded yet.</h1>';
505+
delete componentsPortalContainer._hasInitialHTMLBeenCleared;
506+
}
507+
508+
if (profilerPortalContainer) {
509+
profilerPortalContainer.innerHTML =
510+
'<h1 class="no-react-disclaimer">Looks like this page doesn\'t have React, or it hasn\'t been loaded yet.</h1>';
511+
delete profilerPortalContainer._hasInitialHTMLBeenCleared;
550512
}
513+
}
551514

552-
executeIfReactHasLoaded(onReactReady, 1);
515+
function mountReactDevToolsWhenReactHasLoaded() {
516+
reactPollingInstance = startReactPolling(
517+
mountReactDevTools,
518+
5, // ~5 seconds
519+
showNoReactDisclaimer,
520+
);
553521
}
554522

555523
let bridge = null;
524+
let lastSubscribedBridgeListener = null;
556525
let store = null;
557526

558527
let profilingData = null;
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/* global chrome */
2+
3+
class CouldNotFindReactOnThePageError extends Error {
4+
constructor() {
5+
super("Could not find React, or it hasn't been loaded yet");
6+
7+
// Maintains proper stack trace for where our error was thrown (only available on V8)
8+
if (Error.captureStackTrace) {
9+
Error.captureStackTrace(this, CouldNotFindReactOnThePageError);
10+
}
11+
12+
this.name = 'CouldNotFindReactOnThePageError';
13+
}
14+
}
15+
16+
export function startReactPolling(
17+
onReactFound,
18+
attemptsThreshold,
19+
onCouldNotFindReactAfterReachingAttemptsThreshold,
20+
) {
21+
let status = 'idle';
22+
23+
function abort() {
24+
status = 'aborted';
25+
}
26+
27+
// This function will call onSuccess only if React was found and polling is not aborted, onError will be called for every other case
28+
function checkIfReactPresentInInspectedWindow(onSuccess, onError) {
29+
chrome.devtools.inspectedWindow.eval(
30+
'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0',
31+
(pageHasReact, exceptionInfo) => {
32+
if (status === 'aborted') {
33+
onError(
34+
'Polling was aborted, user probably navigated to the other page',
35+
);
36+
return;
37+
}
38+
39+
if (exceptionInfo) {
40+
const {code, description, isError, isException, value} =
41+
exceptionInfo;
42+
43+
if (isException) {
44+
onError(
45+
`Received error while checking if react has loaded: ${value}`,
46+
);
47+
return;
48+
}
49+
50+
if (isError) {
51+
onError(
52+
`Received error with code ${code} while checking if react has loaded: "${description}"`,
53+
);
54+
return;
55+
}
56+
}
57+
58+
if (pageHasReact) {
59+
onSuccess();
60+
return;
61+
}
62+
63+
onError(new CouldNotFindReactOnThePageError());
64+
},
65+
);
66+
}
67+
68+
// Just a Promise wrapper around `checkIfReactPresentInInspectedWindow`
69+
// returns a Promise, which will resolve only if React has been found on the page
70+
function poll(attempt) {
71+
return new Promise((resolve, reject) => {
72+
checkIfReactPresentInInspectedWindow(resolve, reject);
73+
}).catch(error => {
74+
if (error instanceof CouldNotFindReactOnThePageError) {
75+
if (attempt === attemptsThreshold) {
76+
onCouldNotFindReactAfterReachingAttemptsThreshold();
77+
}
78+
79+
// Start next attempt in 0.5s
80+
return new Promise(r => setTimeout(r, 500)).then(() =>
81+
poll(attempt + 1),
82+
);
83+
}
84+
85+
// Propagating every other Error
86+
throw error;
87+
});
88+
}
89+
90+
poll(1)
91+
.then(onReactFound)
92+
.catch(error => {
93+
// Log propagated errors only if polling was not aborted
94+
// Some errors are expected when user performs in-tab navigation and `.eval()` is still being executed
95+
if (status === 'aborted') {
96+
return;
97+
}
98+
99+
console.error(error);
100+
});
101+
102+
return {abort};
103+
}

0 commit comments

Comments
 (0)