DevTools: Support mulitple DevTools instances per page (#22949)
This is being done so that we can embed DevTools within the new React (beta) docs. The primary changes here are to `react-devtools-inline/backend`: * Add a new `createBridge` API * Add an option to the `activate` method to support passing in the custom bridge object. The `react-devtools-inline` README has been updated to include these new methods. To verify these changes, this commit also updates the test shell to add a new entry-point for multiple DevTools. This commit also replaces two direct calls to `window.postMessage()` with `bridge.send()` (and adds the related Flow types).
Brian Vaughn committed
Dec 14, 2021 at 12:16 UTC
911f92a44da540b3c60779b28745d050ddd21679
16 files changed
+340
-147
packages/react-devtools-inline/README.md
+28
-13
@@ -56,7 +56,7 @@ const iframe = document.getElementById(frameID);
56
const contentWindow = iframe.contentWindow;
57
58
// This returns a React component that can be rendered into your app.
59
-// <DevTools {...props} />
59
+// e.g. render(<DevTools {...props} />);
60
const DevTools = initialize(contentWindow);
61
```
62
@@ -177,32 +177,47 @@ Below is an example of an advanced integration with a website like [Replay.io](h
177
178
```js
179
import {
180
- createBridge,
180
+ activate as activateBackend,
181
+ createBridge as createBackendBridge,
182
+ initialize as initializeBackend,
183
+} from 'react-devtools-inline/backend';
184
+import {
185
+ createBridge as createFrontendBridge,
186
createStore,
187
initialize as createDevTools,
183
-} from "react-devtools-inline/frontend";
188
+} from 'react-devtools-inline/frontend';
189
185
-// Custom Wall implementation enables serializing data
186
-// using an API other than window.postMessage()
190
+// DevTools uses "message" events and window.postMessage() by default,
191
+// but we can override this behavior by creating a custom "Wall" object.
192
// For example...
193
const wall = {
189
- emit() {},
194
+ _listeners: [],
195
listen(listener) {
191
- wall._listener = listener;
196
+ wall._listeners.push(listener);
197
},
193
- async send(event, payload) {
194
- const response = await fetch(...).json();
195
- wall._listener(response);
198
+ send(event, payload) {
199
+ wall._listeners.forEach(listener => listener({event, payload}));
200
},
201
};
202
199
-// Create a Bridge and Store that use the custom Wall.
203
+// Initialize the DevTools backend before importing React (or any other packages that might import React).
204
+initializeBackend(contentWindow);
205
+
206
+// Prepare DevTools for rendering.
207
+// To use the custom Wall we've created, we need to also create our own "Bridge" and "Store" objects.
208
const bridge = createBridge(target, wall);
209
const store = createStore(bridge);
210
const DevTools = createDevTools(target, { bridge, store });
211
204
-// Render DevTools with it.
205
-<DevTools {...otherProps} />;
212
+// You can render DevTools now:
213
+const root = createRoot(container);
214
+root.render(<DevTools {...otherProps} />);
215
+
216
+// Lastly, let the DevTools backend know that the frontend is ready.
217
+// To use the custom Wall we've created, we need to also pass in the "Bridge".
218
+activateBackend(contentWindow, {
219
+ bridge: createBackendBridge(contentWindow, wall),
220
+});
221
```
222
223
## Local development
packages/react-devtools-inline/src/backend.js
+79
-68
@@ -5,83 +5,57 @@ import Bridge from 'react-devtools-shared/src/bridge';
5
import {initBackend} from 'react-devtools-shared/src/backend';
6
import {installHook} from 'react-devtools-shared/src/hook';
7
import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
8
-import {
9
- MESSAGE_TYPE_GET_SAVED_PREFERENCES,
10
- MESSAGE_TYPE_SAVED_PREFERENCES,
11
-} from './constants';
8
13
-function startActivation(contentWindow: window) {
14
- const {parent} = contentWindow;
15
-
16
- const onMessage = ({data}) => {
17
- switch (data.type) {
18
- case MESSAGE_TYPE_SAVED_PREFERENCES:
19
- // This is the only message we're listening for,
20
- // so it's safe to cleanup after we've received it.
21
- contentWindow.removeEventListener('message', onMessage);
22
-
23
- const {
24
- appendComponentStack,
25
- breakOnConsoleErrors,
26
- componentFilters,
27
- showInlineWarningsAndErrors,
28
- hideConsoleLogsInStrictMode,
29
- } = data;
30
-
31
- contentWindow.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = appendComponentStack;
32
- contentWindow.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors;
33
- contentWindow.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
34
- contentWindow.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = showInlineWarningsAndErrors;
35
- contentWindow.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = hideConsoleLogsInStrictMode;
36
-
37
- // TRICKY
38
- // The backend entry point may be required in the context of an iframe or the parent window.
39
- // If it's required within the parent window, store the saved values on it as well,
40
- // since the injected renderer interface will read from window.
41
- // Technically we don't need to store them on the contentWindow in this case,
42
- // but it doesn't really hurt anything to store them there too.
43
- if (contentWindow !== window) {
44
- window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = appendComponentStack;
45
- window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors;
46
- window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
47
- window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = showInlineWarningsAndErrors;
48
- window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = hideConsoleLogsInStrictMode;
49
- }
50
-
51
- finishActivation(contentWindow);
52
- break;
53
- default:
54
- break;
9
+import type {BackendBridge} from 'react-devtools-shared/src/bridge';
10
+import type {Wall} from 'react-devtools-shared/src/types';
11
+
12
+function startActivation(contentWindow: window, bridge: BackendBridge) {
13
+ const onSavedPreferences = data => {
14
+ // This is the only message we're listening for,
15
+ // so it's safe to cleanup after we've received it.
16
+ bridge.removeListener('savedPreferences', onSavedPreferences);
17
+
18
+ const {
19
+ appendComponentStack,
20
+ breakOnConsoleErrors,
21
+ componentFilters,
22
+ showInlineWarningsAndErrors,
23
+ hideConsoleLogsInStrictMode,
24
+ } = data;
25
+
26
+ contentWindow.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = appendComponentStack;
27
+ contentWindow.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors;
28
+ contentWindow.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
29
+ contentWindow.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = showInlineWarningsAndErrors;
30
+ contentWindow.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = hideConsoleLogsInStrictMode;
31
+
32
+ // TRICKY
33
+ // The backend entry point may be required in the context of an iframe or the parent window.
34
+ // If it's required within the parent window, store the saved values on it as well,
35
+ // since the injected renderer interface will read from window.
36
+ // Technically we don't need to store them on the contentWindow in this case,
37
+ // but it doesn't really hurt anything to store them there too.
38
+ if (contentWindow !== window) {
39
+ window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = appendComponentStack;
40
+ window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors;
41
+ window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
42
+ window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = showInlineWarningsAndErrors;
43
+ window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = hideConsoleLogsInStrictMode;
44
}
45
+
46
+ finishActivation(contentWindow, bridge);
47
};
48
58
- contentWindow.addEventListener('message', onMessage);
49
+ bridge.addListener('savedPreferences', onSavedPreferences);
50
51
// The backend may be unable to read saved preferences directly,
52
// because they are stored in localStorage within the context of the extension (on the frontend).
53
// Instead it relies on the extension to pass preferences through.
54
// Because we might be in a sandboxed iframe, we have to ask for them by way of postMessage().
64
- parent.postMessage({type: MESSAGE_TYPE_GET_SAVED_PREFERENCES}, '*');
55
+ bridge.send('getSavedPreferences');
56
}
57
67
-function finishActivation(contentWindow: window) {
68
- const {parent} = contentWindow;
69
-
70
- const bridge = new Bridge({
71
- listen(fn) {
72
- const onMessage = event => {
73
- fn(event.data);
74
- };
75
- contentWindow.addEventListener('message', onMessage);
76
- return () => {
77
- contentWindow.removeEventListener('message', onMessage);
78
- };
79
- },
80
- send(event: string, payload: any, transferable?: Array<any>) {
81
- parent.postMessage({event, payload}, '*', transferable);
82
- },
83
- });
84
-
58
+function finishActivation(contentWindow: window, bridge: BackendBridge) {
59
const agent = new Agent(bridge);
60
61
const hook = contentWindow.__REACT_DEVTOOLS_GLOBAL_HOOK__;
@@ -100,8 +74,45 @@ function finishActivation(contentWindow: window) {
74
}
75
}
76
103
-export function activate(contentWindow: window): void {
104
- startActivation(contentWindow);
77
+export function activate(
78
+ contentWindow: window,
79
+ {
80
+ bridge,
81
+ }: {|
82
+ bridge?: BackendBridge,
83
+ |} = {},
84
+): void {
85
+ if (bridge == null) {
86
+ bridge = createBridge(contentWindow);
87
+ }
88
+
89
+ startActivation(contentWindow, bridge);
90
+}
91
+
92
+export function createBridge(
93
+ contentWindow: window,
94
+ wall?: Wall,
95
+): BackendBridge {
96
+ const {parent} = contentWindow;
97
+
98
+ if (wall == null) {
99
+ wall = {
100
+ listen(fn) {
101
+ const onMessage = ({data}) => {
102
+ fn(data);
103
+ };
104
+ contentWindow.addEventListener('message', onMessage);
105
+ return () => {
106
+ contentWindow.removeEventListener('message', onMessage);
107
+ };
108
+ },
109
+ send(event: string, payload: any, transferable?: Array<any>) {
110
+ parent.postMessage({event, payload}, '*', transferable);
111
+ },
112
+ };
113
+ }
114
+
115
+ return (new Bridge(wall): BackendBridge);
116
}
117
118
export function initialize(contentWindow: window): void {
packages/react-devtools-inline/src/constants.js
deleted
-6
@@ -1,6 +0,0 @@
1
-/** @flow */
2
-
3
-export const MESSAGE_TYPE_GET_SAVED_PREFERENCES =
4
- 'React::DevTools::getSavedPreferences';
5
-export const MESSAGE_TYPE_SAVED_PREFERENCES =
6
- 'React::DevTools::savedPreferences';
packages/react-devtools-inline/src/frontend.js
+26
-39
@@ -12,10 +12,6 @@ import {
12
getShowInlineWarningsAndErrors,
13
getHideConsoleLogsInStrictMode,
14
} from 'react-devtools-shared/src/utils';
15
-import {
16
- MESSAGE_TYPE_GET_SAVED_PREFERENCES,
17
- MESSAGE_TYPE_SAVED_PREFERENCES,
18
-} from './constants';
15
16
import type {Wall} from 'react-devtools-shared/src/types';
17
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
@@ -68,49 +64,40 @@ export function initialize(
64
store?: Store,
65
|} = {},
66
): React.AbstractComponent<Props, mixed> {
71
- const onGetSavedPreferencesMessage = ({data, source}) => {
72
- if (source === 'react-devtools-content-script') {
73
- // Ignore messages from the DevTools browser extension.
74
- }
75
-
76
- switch (data.type) {
77
- case MESSAGE_TYPE_GET_SAVED_PREFERENCES:
78
- // This is the only message we're listening for,
79
- // so it's safe to cleanup after we've received it.
80
- window.removeEventListener('message', onGetSavedPreferencesMessage);
81
-
82
- // The renderer interface can't read saved preferences directly,
83
- // because they are stored in localStorage within the context of the extension.
84
- // Instead it relies on the extension to pass them through.
85
- contentWindow.postMessage(
86
- {
87
- type: MESSAGE_TYPE_SAVED_PREFERENCES,
88
- appendComponentStack: getAppendComponentStack(),
89
- breakOnConsoleErrors: getBreakOnConsoleErrors(),
90
- componentFilters: getSavedComponentFilters(),
91
- showInlineWarningsAndErrors: getShowInlineWarningsAndErrors(),
92
- hideConsoleLogsInStrictMode: getHideConsoleLogsInStrictMode(),
93
- },
94
- '*',
95
- );
96
- break;
97
- default:
98
- break;
99
- }
100
- };
101
-
102
- window.addEventListener('message', onGetSavedPreferencesMessage);
103
-
67
if (bridge == null) {
68
bridge = createBridge(contentWindow);
69
}
70
71
+ // Type refinement.
72
+ const frontendBridge = ((bridge: any): FrontendBridge);
73
+
74
if (store == null) {
109
- store = createStore(bridge);
75
+ store = createStore(frontendBridge);
76
}
77
78
+ const onGetSavedPreferences = () => {
79
+ // This is the only message we're listening for,
80
+ // so it's safe to cleanup after we've received it.
81
+ frontendBridge.removeListener('getSavedPreferences', onGetSavedPreferences);
82
+
83
+ const data = {
84
+ appendComponentStack: getAppendComponentStack(),
85
+ breakOnConsoleErrors: getBreakOnConsoleErrors(),
86
+ componentFilters: getSavedComponentFilters(),
87
+ showInlineWarningsAndErrors: getShowInlineWarningsAndErrors(),
88
+ hideConsoleLogsInStrictMode: getHideConsoleLogsInStrictMode(),
89
+ };
90
+
91
+ // The renderer interface can't read saved preferences directly,
92
+ // because they are stored in localStorage within the context of the extension.
93
+ // Instead it relies on the extension to pass them through.
94
+ frontendBridge.send('savedPreferences', data);
95
+ };
96
+
97
+ frontendBridge.addListener('getSavedPreferences', onGetSavedPreferences);
98
+
99
const ForwardRef = forwardRef<Props, mixed>((props, ref) => (
113
- <DevTools ref={ref} bridge={bridge} store={store} {...props} />
100
+ <DevTools ref={ref} bridge={frontendBridge} store={store} {...props} />
101
));
102
ForwardRef.displayName = 'DevTools';
103
packages/react-devtools-shared/src/backend/agent.js
+3
@@ -225,6 +225,9 @@ export default class Agent extends EventEmitter<{|
225
bridge.send('profilingStatus', true);
226
}
227
228
+ // Send the Bridge protocol after initialization in case the frontend has already requested it.
229
+ this._bridge.send('bridgeProtocol', currentBridgeProtocol);
230
+
231
// Notify the frontend if the backend supports the Storage API (e.g. localStorage).
232
// If not, features like reload-and-profile will not work correctly and must be disabled.
233
let isBackendStorageAPISupported = false;
packages/react-devtools-shared/src/bridge.js
+13
-1
@@ -176,10 +176,19 @@ type UpdateConsolePatchSettingsParams = {|
176
browserTheme: BrowserTheme,
177
|};
178
179
+type SavedPreferencesParams = {|
180
+ appendComponentStack: boolean,
181
+ breakOnConsoleErrors: boolean,
182
+ componentFilters: Array<ComponentFilter>,
183
+ showInlineWarningsAndErrors: boolean,
184
+ hideConsoleLogsInStrictMode: boolean,
185
+|};
186
+
187
export type BackendEvents = {|
188
bridgeProtocol: [BridgeProtocol],
189
extensionBackendInitialized: [],
190
fastRefreshScheduled: [],
191
+ getSavedPreferences: [],
192
inspectedElement: [InspectedElementPayload],
193
isBackendStorageAPISupported: [boolean],
194
isSynchronousXHRSupported: [boolean],
@@ -223,6 +232,7 @@ type FrontendEvents = {|
232
profilingData: [ProfilingDataBackend],
233
reloadAndProfile: [boolean],
234
renamePath: [RenamePath],
235
+ savedPreferences: [SavedPreferencesParams],
236
selectFiber: [number],
237
setTraceUpdatesEnabled: [boolean],
238
shutdown: [],
@@ -277,7 +287,9 @@ class Bridge<
287
288
this._wallUnlisten =
289
wall.listen((message: Message) => {
280
- (this: any).emit(message.event, message.payload);
290
+ if (message && message.event) {
291
+ (this: any).emit(message.event, message.payload);
292
+ }
293
}) || null;
294
295
// Temporarily support older standalone front-ends sending commands to newer embedded backends.
packages/react-devtools-shell/app.html
renamed
+1
-1
@@ -64,6 +64,6 @@
64
<!-- This script installs the hook, injects the backend, and renders the DevTools UI -->
65
<!-- In DEV mode, this file is served by the Webpack dev server -->
66
<!-- For production builds, it's built by Webpack and uploaded from the local file system -->
67
- <script src="dist/devtools.js"></script>
67
+ <script src="dist/app-devtools.js"></script>
68
</body>
69
</html>
\ No newline at end of file
packages/react-devtools-shell/multi.html
new
+57
@@ -0,0 +1,57 @@
1
+<!doctype html>
2
+<html>
3
+ <head>
4
+ <meta charset="utf8">
5
+ <title>React DevTools</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <style>
8
+ * {
9
+ box-sizing: border-box;
10
+ }
11
+ body {
12
+ display: flex;
13
+ flex-direction: row;
14
+ position: absolute;
15
+ top: 0;
16
+ left: 0;
17
+ right: 0;
18
+ bottom: 0;
19
+ margin: 0;
20
+ padding: 0;
21
+ font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,
22
+ sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
23
+ font-size: 12px;
24
+ line-height: 1.5;
25
+ }
26
+ .column {
27
+ display: flex;
28
+ flex-direction: column;
29
+ flex: 1 1 50%;
30
+ }
31
+ .column:first-of-type {
32
+ border-right: 1px solid #3d424a;
33
+ }
34
+ .iframe {
35
+ height: 50%;
36
+ flex: 0 0 50%;
37
+ border: none;
38
+ }
39
+ .devtools {
40
+ height: 50%;
41
+ flex: 0 0 50%;
42
+ }
43
+ </style>
44
+ </head>
45
+ <body>
46
+ <div class="column left-column">
47
+ <iframe id="iframe-left" class="iframe"></iframe>
48
+ <div id="devtools-left" class="devtools"></div>
49
+ </div>
50
+ <div class="column">
51
+ <iframe id="iframe-right" class="iframe"></iframe>
52
+ <div id="devtools-right" class="devtools"></div>
53
+ </div>
54
+
55
+ <script src="dist/multi-devtools.js"></script>
56
+ </body>
57
+</html>
\ No newline at end of file
packages/react-devtools-shell/now.json
deleted
-5
@@ -1,5 +0,0 @@
1
-{
2
- "name": "react-devtools-experimental",
3
- "alias": ["react-devtools-experimental"],
4
- "files": ["index.html", "dist"]
5
-}
packages/react-devtools-shell/package.json
+3
-3
@@ -3,9 +3,9 @@
3
"name": "react-devtools-shell",
4
"version": "0.0.0",
5
"scripts": {
6
- "build": "cross-env NODE_ENV=development cross-env TARGET=remote webpack --config webpack.config.js",
7
- "deploy": "yarn run build && now deploy && now alias react-devtools-experimental",
8
- "start": "cross-env NODE_ENV=development cross-env TARGET=local webpack-dev-server --open"
6
+ "start": "yarn start:app",
7
+ "start:app": "cross-env NODE_ENV=development cross-env TARGET=local webpack-dev-server --open-page app.html",
8
+ "start:multi": "cross-env NODE_ENV=development cross-env TARGET=local webpack-dev-server --open-page multi.html"
9
},
10
"dependencies": {
11
"immutable": "^4.0.0-rc.12",
packages/react-devtools-shell/src/app/devtools.js
renamed
+1
-1
@@ -60,7 +60,7 @@ function hookNamesModuleLoaderFunction() {
60
return import('react-devtools-inline/hookNames');
61
}
62
63
-inject('dist/app.js', () => {
63
+inject('dist/app-index.js', () => {
64
initDevTools({
65
connect(cb) {
66
const root = createRoot(container);
packages/react-devtools-shell/src/multi/devtools.js
new
+71
@@ -0,0 +1,71 @@
1
+import * as React from 'react';
2
+import {createRoot} from 'react-dom';
3
+import {
4
+ activate as activateBackend,
5
+ createBridge as createBackendBridge,
6
+ initialize as initializeBackend,
7
+} from 'react-devtools-inline/backend';
8
+import {
9
+ createBridge as createFrontendBridge,
10
+ createStore,
11
+ initialize as createDevTools,
12
+} from 'react-devtools-inline/frontend';
13
+import {__DEBUG__} from 'react-devtools-shared/src/constants';
14
+
15
+function inject(contentDocument, sourcePath, callback) {
16
+ const script = contentDocument.createElement('script');
17
+ script.onload = callback;
18
+ script.src = sourcePath;
19
+
20
+ ((contentDocument.body: any): HTMLBodyElement).appendChild(script);
21
+}
22
+
23
+function init(appIframe, devtoolsContainer, appSource) {
24
+ const {contentDocument, contentWindow} = appIframe;
25
+
26
+ // Wire each DevTools instance directly to its app.
27
+ // By default, DevTools dispatches "message" events on the window,
28
+ // but this means that only one instance of DevTools can live on a page.
29
+ const wall = {
30
+ _listeners: [],
31
+ listen(listener) {
32
+ if (__DEBUG__) {
33
+ console.log('[Shell] Wall.listen()');
34
+ }
35
+
36
+ wall._listeners.push(listener);
37
+ },
38
+ send(event, payload) {
39
+ if (__DEBUG__) {
40
+ console.log('[Shell] Wall.send()', {event, payload});
41
+ }
42
+
43
+ wall._listeners.forEach(listener => listener({event, payload}));
44
+ },
45
+ };
46
+
47
+ const backendBridge = createBackendBridge(contentWindow, wall);
48
+
49
+ initializeBackend(contentWindow);
50
+
51
+ const frontendBridge = createFrontendBridge(contentWindow, wall);
52
+ const store = createStore(frontendBridge);
53
+ const DevTools = createDevTools(contentWindow, {
54
+ bridge: frontendBridge,
55
+ store,
56
+ });
57
+
58
+ inject(contentDocument, appSource, () => {
59
+ createRoot(devtoolsContainer).render(<DevTools />);
60
+ });
61
+
62
+ activateBackend(contentWindow, {bridge: backendBridge});
63
+}
64
+
65
+const appIframeLeft = document.getElementById('iframe-left');
66
+const appIframeRight = document.getElementById('iframe-right');
67
+const devtoolsContainerLeft = document.getElementById('devtools-left');
68
+const devtoolsContainerRight = document.getElementById('devtools-right');
69
+
70
+init(appIframeLeft, devtoolsContainerLeft, 'dist/multi-left.js');
71
+init(appIframeRight, devtoolsContainerRight, 'dist/multi-right.js');
packages/react-devtools-shell/src/multi/left.js
new
+19
@@ -0,0 +1,19 @@
1
+import * as React from 'react';
2
+import {useState} from 'react';
3
+import {createRoot} from 'react-dom';
4
+
5
+function createContainer() {
6
+ const container = document.createElement('div');
7
+
8
+ ((document.body: any): HTMLBodyElement).appendChild(container);
9
+
10
+ return container;
11
+}
12
+
13
+function StatefulCounter() {
14
+ const [count, setCount] = useState(0);
15
+ const handleClick = () => setCount(count + 1);
16
+ return <button onClick={handleClick}>Count {count}</button>;
17
+}
18
+
19
+createRoot(createContainer()).render(<StatefulCounter />);
packages/react-devtools-shell/src/multi/right.js
new
+33
@@ -0,0 +1,33 @@
1
+import * as React from 'react';
2
+import {useLayoutEffect, useRef, useState} from 'react';
3
+import {render} from 'react-dom';
4
+
5
+function createContainer() {
6
+ const container = document.createElement('div');
7
+
8
+ ((document.body: any): HTMLBodyElement).appendChild(container);
9
+
10
+ return container;
11
+}
12
+
13
+function EffectWithState() {
14
+ const [didMount, setDidMount] = useState(0);
15
+
16
+ const renderCountRef = useRef(0);
17
+ renderCountRef.current++;
18
+
19
+ useLayoutEffect(() => {
20
+ if (!didMount) {
21
+ setDidMount(true);
22
+ }
23
+ }, [didMount]);
24
+
25
+ return (
26
+ <ul>
27
+ <li>Rendered {renderCountRef.current} times</li>
28
+ {didMount && <li>Mounted!</li>}
29
+ </ul>
30
+ );
31
+}
32
+
33
+render(<EffectWithState />, createContainer());
packages/react-devtools-shell/webpack.config.js
+5
-2
@@ -42,8 +42,11 @@ const config = {
42
mode: __DEV__ ? 'development' : 'production',
43
devtool: __DEV__ ? 'cheap-source-map' : 'source-map',
44
entry: {
45
- app: './src/app/index.js',
46
- devtools: './src/devtools.js',
45
+ 'app-index': './src/app/index.js',
46
+ 'app-devtools': './src/app/devtools.js',
47
+ 'multi-left': './src/multi/left.js',
48
+ 'multi-devtools': './src/multi/devtools.js',
49
+ 'multi-right': './src/multi/right.js',
50
},
51
node: {
52
// source-maps package has a dependency on 'fs'
packages/react-devtools/package.json
+1
-8
@@ -11,14 +11,7 @@
11
"bin": {
12
"react-devtools": "./bin.js"
13
},
14
- "files": [
15
- "bin.js",
16
- "build-info.json",
17
- "app.html",
18
- "app.js",
19
- "index.js",
20
- "icons"
21
- ],
14
+ "files": [],
15
"scripts": {
16
"start": "node bin.js"
17
},