@samitouri / QOS-React / commits / 911f92a44d

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 },