Skip to content

Commit bb6b86e

Browse files
authored
refactor[react-devtools]: initialize renderer interface early (#30946)
The current state is that `rendererInterface`, which contains all the backend logic, like generating component stack or attaching errors to fibers, or traversing the Fiber tree, ..., is only mounted after the Frontend is created. For browser extension, this means that we don't patch console or track errors and warnings before Chrome DevTools is opened. With these changes, `rendererInterface` is created right after `renderer` is injected from React via global hook object (e. g. `__REACT_DEVTOOLS_GLOBAL_HOOK__.inject(...)`. Because of the current implementation, in case of multiple Reacts on the page, all of them will patch the console independently. This will be fixed in one of the next PRs, where I am moving console patching to the global Hook. This change of course makes `hook.js` script bigger, but I think it is a reasonable trade-off for better DevX. We later can add more heuristics to optimize the performance (if necessary) of `rendererInterface` for cases when Frontend was connected late and Backend is attempting to flush out too many recorded operations. This essentially reverts #26563.
1 parent d6cb4e7 commit bb6b86e

File tree

10 files changed

+110
-134
lines changed

10 files changed

+110
-134
lines changed

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

-8
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,6 @@ const contentScriptsToInject = [
2525
runAt: 'document_start',
2626
world: chrome.scripting.ExecutionWorld.MAIN,
2727
},
28-
{
29-
id: '@react-devtools/renderer',
30-
js: ['build/renderer.js'],
31-
matches: ['<all_urls>'],
32-
persistAcrossSessions: true,
33-
runAt: 'document_start',
34-
world: chrome.scripting.ExecutionWorld.MAIN,
35-
},
3628
];
3729

3830
async function dynamicallyInjectContentScripts() {

packages/react-devtools-extensions/src/contentScripts/renderer.js

-33
This file was deleted.

packages/react-devtools-extensions/webpack.config.js

-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ module.exports = {
5555
panel: './src/panel.js',
5656
proxy: './src/contentScripts/proxy.js',
5757
prepareInjection: './src/contentScripts/prepareInjection.js',
58-
renderer: './src/contentScripts/renderer.js',
5958
installHook: './src/contentScripts/installHook.js',
6059
},
6160
output: {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type {
11+
ReactRenderer,
12+
RendererInterface,
13+
DevToolsHook,
14+
RendererID,
15+
} from 'react-devtools-shared/src/backend/types';
16+
17+
import {attach as attachFlight} from 'react-devtools-shared/src/backend/flight/renderer';
18+
import {attach as attachFiber} from 'react-devtools-shared/src/backend/fiber/renderer';
19+
import {attach as attachLegacy} from 'react-devtools-shared/src/backend/legacy/renderer';
20+
import {hasAssignedBackend} from 'react-devtools-shared/src/backend/utils';
21+
22+
// this is the backend that is compatible with all older React versions
23+
function isMatchingRender(version: string): boolean {
24+
return !hasAssignedBackend(version);
25+
}
26+
27+
export default function attachRenderer(
28+
hook: DevToolsHook,
29+
id: RendererID,
30+
renderer: ReactRenderer,
31+
global: Object,
32+
): RendererInterface | void {
33+
// only attach if the renderer is compatible with the current version of the backend
34+
if (!isMatchingRender(renderer.reconcilerVersion || renderer.version)) {
35+
return;
36+
}
37+
let rendererInterface = hook.rendererInterfaces.get(id);
38+
39+
// Inject any not-yet-injected renderers (if we didn't reload-and-profile)
40+
if (rendererInterface == null) {
41+
if (typeof renderer.getCurrentComponentInfo === 'function') {
42+
// react-flight/client
43+
rendererInterface = attachFlight(hook, id, renderer, global);
44+
} else if (
45+
// v16-19
46+
typeof renderer.findFiberByHostInstance === 'function' ||
47+
// v16.8+
48+
renderer.currentDispatcherRef != null
49+
) {
50+
// react-reconciler v16+
51+
rendererInterface = attachFiber(hook, id, renderer, global);
52+
} else if (renderer.ComponentTree) {
53+
// react-dom v15
54+
rendererInterface = attachLegacy(hook, id, renderer, global);
55+
} else {
56+
// Older react-dom or other unsupported renderer version
57+
}
58+
}
59+
60+
return rendererInterface;
61+
}

packages/react-devtools-shared/src/backend/agent.js

+12-3
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ export default class Agent extends EventEmitter<{
152152
traceUpdates: [Set<HostInstance>],
153153
drawTraceUpdates: [Array<HostInstance>],
154154
disableTraceUpdates: [],
155+
getIfHasUnsupportedRendererVersion: [],
155156
}> {
156157
_bridge: BackendBridge;
157158
_isProfiling: boolean = false;
@@ -221,6 +222,10 @@ export default class Agent extends EventEmitter<{
221222
);
222223
bridge.addListener('updateComponentFilters', this.updateComponentFilters);
223224
bridge.addListener('getEnvironmentNames', this.getEnvironmentNames);
225+
bridge.addListener(
226+
'getIfHasUnsupportedRendererVersion',
227+
this.getIfHasUnsupportedRendererVersion,
228+
);
224229

225230
// Temporarily support older standalone front-ends sending commands to newer embedded backends.
226231
// We do this because React Native embeds the React DevTools backend,
@@ -709,7 +714,7 @@ export default class Agent extends EventEmitter<{
709714
}
710715
}
711716

712-
setRendererInterface(
717+
registerRendererInterface(
713718
rendererID: RendererID,
714719
rendererInterface: RendererInterface,
715720
) {
@@ -940,8 +945,12 @@ export default class Agent extends EventEmitter<{
940945
}
941946
};
942947

943-
onUnsupportedRenderer(rendererID: number) {
944-
this._bridge.send('unsupportedRendererVersion', rendererID);
948+
getIfHasUnsupportedRendererVersion: () => void = () => {
949+
this.emit('getIfHasUnsupportedRendererVersion');
950+
};
951+
952+
onUnsupportedRenderer() {
953+
this._bridge.send('unsupportedRendererVersion');
945954
}
946955

947956
_persistSelectionTimerScheduled: boolean = false;

packages/react-devtools-shared/src/backend/index.js

+22-79
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,7 @@
99

1010
import Agent from './agent';
1111

12-
import {attach as attachFiber} from './fiber/renderer';
13-
import {attach as attachFlight} from './flight/renderer';
14-
import {attach as attachLegacy} from './legacy/renderer';
15-
16-
import {hasAssignedBackend} from './utils';
17-
18-
import type {DevToolsHook, ReactRenderer, RendererInterface} from './types';
19-
20-
// this is the backend that is compatible with all older React versions
21-
function isMatchingRender(version: string): boolean {
22-
return !hasAssignedBackend(version);
23-
}
12+
import type {DevToolsHook, RendererID, RendererInterface} from './types';
2413

2514
export type InitBackend = typeof initBackend;
2615

@@ -34,29 +23,32 @@ export function initBackend(
3423
return () => {};
3524
}
3625

26+
function registerRendererInterface(
27+
id: RendererID,
28+
rendererInterface: RendererInterface,
29+
) {
30+
agent.registerRendererInterface(id, rendererInterface);
31+
32+
// Now that the Store and the renderer interface are connected,
33+
// it's time to flush the pending operation codes to the frontend.
34+
rendererInterface.flushInitialOperations();
35+
}
36+
3737
const subs = [
3838
hook.sub(
3939
'renderer-attached',
4040
({
4141
id,
42-
renderer,
4342
rendererInterface,
4443
}: {
4544
id: number,
46-
renderer: ReactRenderer,
4745
rendererInterface: RendererInterface,
48-
...
4946
}) => {
50-
agent.setRendererInterface(id, rendererInterface);
51-
52-
// Now that the Store and the renderer interface are connected,
53-
// it's time to flush the pending operation codes to the frontend.
54-
rendererInterface.flushInitialOperations();
47+
registerRendererInterface(id, rendererInterface);
5548
},
5649
),
57-
58-
hook.sub('unsupported-renderer-version', (id: number) => {
59-
agent.onUnsupportedRenderer(id);
50+
hook.sub('unsupported-renderer-version', () => {
51+
agent.onUnsupportedRenderer();
6052
}),
6153

6254
hook.sub('fastRefreshScheduled', agent.onFastRefreshScheduled),
@@ -66,68 +58,19 @@ export function initBackend(
6658
// TODO Add additional subscriptions required for profiling mode
6759
];
6860

69-
const attachRenderer = (id: number, renderer: ReactRenderer) => {
70-
// only attach if the renderer is compatible with the current version of the backend
71-
if (!isMatchingRender(renderer.reconcilerVersion || renderer.version)) {
72-
return;
73-
}
74-
let rendererInterface = hook.rendererInterfaces.get(id);
75-
76-
// Inject any not-yet-injected renderers (if we didn't reload-and-profile)
77-
if (rendererInterface == null) {
78-
if (typeof renderer.getCurrentComponentInfo === 'function') {
79-
// react-flight/client
80-
rendererInterface = attachFlight(hook, id, renderer, global);
81-
} else if (
82-
// v16-19
83-
typeof renderer.findFiberByHostInstance === 'function' ||
84-
// v16.8+
85-
renderer.currentDispatcherRef != null
86-
) {
87-
// react-reconciler v16+
88-
rendererInterface = attachFiber(hook, id, renderer, global);
89-
} else if (renderer.ComponentTree) {
90-
// react-dom v15
91-
rendererInterface = attachLegacy(hook, id, renderer, global);
92-
} else {
93-
// Older react-dom or other unsupported renderer version
94-
}
95-
96-
if (rendererInterface != null) {
97-
hook.rendererInterfaces.set(id, rendererInterface);
98-
}
61+
agent.addListener('getIfHasUnsupportedRendererVersion', () => {
62+
if (hook.hasUnsupportedRendererAttached) {
63+
agent.onUnsupportedRenderer();
9964
}
100-
101-
// Notify the DevTools frontend about new renderers.
102-
// This includes any that were attached early (via __REACT_DEVTOOLS_ATTACH__).
103-
if (rendererInterface != null) {
104-
hook.emit('renderer-attached', {
105-
id,
106-
renderer,
107-
rendererInterface,
108-
});
109-
} else {
110-
hook.emit('unsupported-renderer-version', id);
111-
}
112-
};
113-
114-
// Connect renderers that have already injected themselves.
115-
hook.renderers.forEach((renderer, id) => {
116-
attachRenderer(id, renderer);
11765
});
11866

119-
// Connect any new renderers that injected themselves.
120-
subs.push(
121-
hook.sub(
122-
'renderer',
123-
({id, renderer}: {id: number, renderer: ReactRenderer, ...}) => {
124-
attachRenderer(id, renderer);
125-
},
126-
),
127-
);
67+
hook.rendererInterfaces.forEach((rendererInterface, id) => {
68+
registerRendererInterface(id, rendererInterface);
69+
});
12870

12971
hook.emit('react-devtools', agent);
13072
hook.reactDevtoolsAgent = agent;
73+
13174
const onAgentShutdown = () => {
13275
subs.forEach(fn => fn());
13376
hook.rendererInterfaces.forEach(rendererInterface => {

packages/react-devtools-shared/src/backend/types.js

+1
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,7 @@ export type DevToolsHook = {
492492
listeners: {[key: string]: Array<Handler>, ...},
493493
rendererInterfaces: Map<RendererID, RendererInterface>,
494494
renderers: Map<RendererID, ReactRenderer>,
495+
hasUnsupportedRendererAttached: boolean,
495496
backends: Map<string, DevToolsBackend>,
496497

497498
emit: (event: string, data: any) => void,

packages/react-devtools-shared/src/bridge.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ export type BackendEvents = {
200200
stopInspectingHost: [boolean],
201201
syncSelectionFromBuiltinElementsPanel: [],
202202
syncSelectionToBuiltinElementsPanel: [],
203-
unsupportedRendererVersion: [RendererID],
203+
unsupportedRendererVersion: [],
204204

205205
// React Native style editor plug-in.
206206
isNativeStyleEditorSupported: [
@@ -218,6 +218,7 @@ type FrontendEvents = {
218218
deletePath: [DeletePath],
219219
getBackendVersion: [],
220220
getBridgeProtocol: [],
221+
getIfHasUnsupportedRendererVersion: [],
221222
getOwnersList: [ElementAndRendererID],
222223
getProfilingData: [{rendererID: RendererID}],
223224
getProfilingStatus: [],

packages/react-devtools-shared/src/devtools/store.js

+1
Original file line numberDiff line numberDiff line change
@@ -1500,6 +1500,7 @@ export default class Store extends EventEmitter<{
15001500
}
15011501

15021502
this._bridge.send('getBackendVersion');
1503+
this._bridge.send('getIfHasUnsupportedRendererVersion');
15031504
};
15041505

15051506
// The Store should never throw an Error without also emitting an event.

packages/react-devtools-shared/src/hook.js

+11-9
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
FIREFOX_CONSOLE_DIMMING_COLOR,
2222
ANSI_STYLE_DIMMING_TEMPLATE,
2323
} from 'react-devtools-shared/src/constants';
24+
import attachRenderer from './attachRenderer';
2425

2526
declare var window: any;
2627

@@ -358,7 +359,6 @@ export function installHook(target: any): DevToolsHook | null {
358359
}
359360

360361
let uidCounter = 0;
361-
362362
function inject(renderer: ReactRenderer): number {
363363
const id = ++uidCounter;
364364
renderers.set(id, renderer);
@@ -367,20 +367,21 @@ export function installHook(target: any): DevToolsHook | null {
367367
? 'deadcode'
368368
: detectReactBuildType(renderer);
369369

370-
// If we have just reloaded to profile, we need to inject the renderer interface before the app loads.
371-
// Otherwise the renderer won't yet exist and we can skip this step.
372-
const attach = target.__REACT_DEVTOOLS_ATTACH__;
373-
if (typeof attach === 'function') {
374-
const rendererInterface = attach(hook, id, renderer, target);
375-
hook.rendererInterfaces.set(id, rendererInterface);
376-
}
377-
378370
hook.emit('renderer', {
379371
id,
380372
renderer,
381373
reactBuildType,
382374
});
383375

376+
const rendererInterface = attachRenderer(hook, id, renderer, target);
377+
if (rendererInterface != null) {
378+
hook.rendererInterfaces.set(id, rendererInterface);
379+
hook.emit('renderer-attached', {id, rendererInterface});
380+
} else {
381+
hook.hasUnsupportedRendererAttached = true;
382+
hook.emit('unsupported-renderer-version');
383+
}
384+
384385
return id;
385386
}
386387

@@ -534,6 +535,7 @@ export function installHook(target: any): DevToolsHook | null {
534535

535536
// Fast Refresh for web relies on this.
536537
renderers,
538+
hasUnsupportedRendererAttached: false,
537539

538540
emit,
539541
getFiberRoots,

0 commit comments

Comments
 (0)