main
md 363 lines 11.9 KB
Rendered Raw
1 # `react-devtools-inline`
2
3 This package can be used to embed React DevTools into browser-based tools like [CodeSandbox](https://codesandbox.io/), [StackBlitz](https://stackblitz.com/), and [Replay](https://replay.io).
4
5 If you're looking for the standalone React DevTools UI, **we suggest using [`react-devtools`](https://github.com/facebook/react/tree/main/packages/react-devtools) instead of using this package directly**.
6
7 ---
8
9 > **Note** that this package (and the DevTools UI) relies on several _experimental_ APIs that are **only available in the [experimental release channel](https://reactjs.org/docs/release-channels.html#experimental-channel)**. This means that you will need to install `react@experimental` and `react-dom@experimental`.
10
11 ---
12
13 # Usage
14
15 This package exports two entry points: a frontend (to be run in the main `window`) and a backend (to be installed and run within an `iframe`<sup>1</sup>).
16
17 The frontend and backend can be initialized in any order, but **the backend must not be activated until the frontend initialization has completed**. Because of this, the simplest sequence is:
18
19 1. Frontend (DevTools interface) initialized in the main `window`.
20 1. Backend initialized in an `iframe`.
21 1. Backend activated.
22
23 <sup>1</sup> Sandboxed iframes are supported.
24
25 # Backend APIs
26 ### `initialize(windowOrGlobal)`
27
28 Installs the global hook on the window/global object. This hook is how React and DevTools communicate.
29
30 > **This method must be called before React is loaded.** (This includes `import`/`require` statements and `<script>` tags that include React.)
31
32 ### `activate(windowOrGlobal)`
33
34 Lets the backend know when the frontend is ready. It should not be called until after the frontend has been initialized, else the frontend might miss important tree-initialization events.
35
36 ### Example
37 ```js
38 import { activate, initialize } from 'react-devtools-inline/backend';
39
40 // This should be the iframe the React application is running in.
41 const iframe = document.getElementById(frameID);
42 const contentWindow = iframe.contentWindow;
43
44 // Call this before importing React (or any other packages that might import React).
45 initialize(contentWindow);
46
47 // Initialize the frontend...
48
49 // Call this only once the frontend has been initialized.
50 activate(contentWindow);
51 ```
52
53 # Frontend APIs
54
55 ### `initialize(windowOrGlobal)`
56 Configures the DevTools interface to listen to the `window` (or `global` object) the backend was injected into. This method returns a React component that can be rendered directly.
57
58 > Because the DevTools interface makes use of several new React concurrent features (like Suspense) **it should be rendered using `ReactDOMClient.createRoot` instead of `ReactDOM.render`.**
59
60 ### Example
61 ```js
62 import { initialize } from 'react-devtools-inline/frontend';
63
64 // This should be the iframe the backend hook has been installed in.
65 const iframe = document.getElementById(frameID);
66 const contentWindow = iframe.contentWindow;
67
68 // This returns a React component that can be rendered into your app.
69 // e.g. render(<DevTools {...props} />);
70 const DevTools = initialize(contentWindow);
71 ```
72
73 # Advanced examples
74
75 ### Supporting named hooks
76
77 DevTools can display hook "names" for an inspected component, although determining the "names" requires loading the source (and source-maps), parsing the code, and inferring the names based on which variables hook values get assigned to. Because the code for this is non-trivial, it's lazy-loaded only if the feature is enabled.
78
79 To configure this package to support this functionality, you'll need to provide a prop that dynamically imports the extra functionality:
80 ```js
81 // Follow code examples above to configure the backend and frontend.
82 // When rendering DevTools, the important part is to pass a 'hookNamesModuleLoaderFunction' prop.
83 const hookNamesModuleLoaderFunction = () => import('react-devtools-inline/hookNames');
84
85 // Render:
86 <DevTools
87 hookNamesModuleLoaderFunction={hookNamesModuleLoaderFunction}
88 {...otherProps}
89 />;
90 ```
91
92 ### Configuring a same-origin `iframe`
93
94 The simplest way to use this package is to install the hook from the parent `window`. This is possible if the `iframe` is not sandboxed and there are no cross-origin restrictions.
95
96 ```js
97 import {
98 activate as activateBackend,
99 initialize as initializeBackend
100 } from 'react-devtools-inline/backend';
101 import { initialize as initializeFrontend } from 'react-devtools-inline/frontend';
102
103 // The React app you want to inspect with DevTools is running within this iframe:
104 const iframe = document.getElementById('target');
105 const { contentWindow } = iframe;
106
107 // Installs the global hook into the iframe.
108 // This must be called before React is loaded into that frame.
109 initializeBackend(contentWindow);
110
111 // Initialize DevTools UI to listen to the hook we just installed.
112 // This returns a React component we can render anywhere in the parent window.
113 // This also must be called before React is loaded into the iframe
114 const DevTools = initializeFrontend(contentWindow);
115
116 // React application can be injected into <iframe> at any time now...
117 // Note that this would need to be done via <script> tag injection,
118 // as setting the src of the <iframe> would load a new page (without the injected backend).
119
120 // <DevTools /> interface can be rendered in the parent window at any time now...
121 // Be sure to use ReactDOMClient.createRoot() to render this component.
122
123 // Let the backend know the frontend is ready and listening.
124 activateBackend(contentWindow);
125 ```
126
127 ### Configuring a sandboxed `iframe`
128
129 Sandboxed `iframe`s are also supported but require more complex initialization.
130
131 **`iframe.html`**
132 ```js
133 import { activate, initialize } from "react-devtools-inline/backend";
134
135 // The DevTools hook needs to be installed before React is even required!
136 // The safest way to do this is probably to install it in a separate script tag.
137 initialize(window);
138
139 // Wait for the frontend to let us know that it's ready.
140 function onMessage({ data }) {
141 switch (data.type) {
142 case "activate-backend":
143 window.removeEventListener("message", onMessage);
144
145 activate(window);
146 break;
147 default:
148 break;
149 }
150 }
151
152 window.addEventListener("message", onMessage);
153 ```
154
155 **`main-window.html`**
156 ```js
157 import { initialize } from "react-devtools-inline/frontend";
158
159 const iframe = document.getElementById("target");
160 const { contentWindow } = iframe;
161
162 // Initialize DevTools UI to listen to the iframe.
163 // This returns a React component we can render anywhere in the main window.
164 // Be sure to use ReactDOMClient.createRoot() to render this component.
165 const DevTools = initialize(contentWindow);
166
167 // Let the backend know to initialize itself.
168 // We can't do this directly because the iframe is sandboxed.
169 // Only initialize the backend once the DevTools frontend has been initialized.
170 iframe.onload = () => {
171 contentWindow.postMessage(
172 {
173 type: "activate-backend"
174 },
175 "*"
176 );
177 };
178 ```
179
180 ### Advanced: Custom "wall"
181
182 Below is an example of an advanced integration with a website like [Replay.io](https://replay.io/) or Code Sandbox's Sandpack (where more than one DevTools instance may be rendered per page).
183
184 ```js
185 import {
186 activate as activateBackend,
187 createBridge as createBackendBridge,
188 initialize as initializeBackend,
189 } from 'react-devtools-inline/backend';
190 import {
191 createBridge as createFrontendBridge,
192 createStore,
193 initialize as createDevTools,
194 } from 'react-devtools-inline/frontend';
195
196 // DevTools uses "message" events and window.postMessage() by default,
197 // but we can override this behavior by creating a custom "Wall" object.
198 // For example...
199 const wall = {
200 _listeners: [],
201 listen(listener) {
202 wall._listeners.push(listener);
203 },
204 send(event, payload) {
205 wall._listeners.forEach(listener => listener({event, payload}));
206 },
207 };
208
209 // Initialize the DevTools backend before importing React (or any other packages that might import React).
210 initializeBackend(contentWindow);
211
212 // Prepare DevTools for rendering.
213 // To use the custom Wall we've created, we need to also create our own "Bridge" and "Store" objects.
214 const bridge = createFrontendBridge(contentWindow, wall);
215 const store = createStore(bridge);
216 const DevTools = createDevTools(contentWindow, { bridge, store });
217
218 // You can render DevTools now:
219 const root = createRoot(container);
220 root.render(<DevTools {...otherProps} />);
221
222 // Lastly, let the DevTools backend know that the frontend is ready.
223 // To use the custom Wall we've created, we need to also pass in the "Bridge".
224 activateBackend(contentWindow, {
225 bridge: createBackendBridge(contentWindow, wall),
226 });
227 ```
228
229 Alternately, if your code can't share the same `wall` object, you can still provide a custom Wall that connects a specific DevTools frontend to a specific backend like so:
230 ```js
231 const uid = "some-unique-string-shared-between-both-pieces";
232 const wall = {
233 listen(listener) {
234 window.addEventListener("message", (event) => {
235 if (event.data.uid === uid) {
236 listener(event.data);
237 }
238 });
239 },
240 send(event, payload) {
241 window.postMessage({ event, payload, uid }, "*");
242 },
243 };
244 ```
245
246 ### Advanced: Node + browser
247
248 Below is an example of an advanced integration that could be used to connect React running in a Node process to React DevTools running in a browser.
249
250 ##### Sample Node backend
251 ```js
252 const {
253 activate,
254 createBridge,
255 initialize,
256 } = require('react-devtools-inline/backend');
257 const { createServer } = require('http');
258 const SocketIO = require('socket.io');
259
260 const server = createServer();
261 const socket = SocketIO(server, {
262 cors: {
263 origin: "*",
264 methods: ["GET", "POST"],
265 allowedHeaders: [],
266 credentials: true
267 }
268 });
269 socket.on('connection', client => {
270 const wall = {
271 listen(listener) {
272 client.on('message', data => {
273 if (data.uid === UID) {
274 listener(data);
275 }
276 });
277 },
278 send(event, payload) {
279 const data = {event, payload, uid: UID};
280 client.emit('message', data);
281 },
282 };
283
284 const bridge = createBridge(global, wall);
285
286 client.on('disconnect', () => {
287 bridge.shutdown();
288 });
289
290 activate(global, { bridge });
291 });
292 socket.listen(PORT);
293 ```
294
295 ##### Sample Web frontend
296 ```js
297 import { createElement } from 'react';
298 import { createRoot } from 'react-dom/client';
299 import {
300 createBridge,
301 createStore,
302 initialize as createDevTools,
303 } from 'react-devtools-inline/frontend';
304 import { io } from "socket.io-client";
305
306 let root = null;
307
308 const socket = io(`http://${HOST}:${PORT}`);
309 socket.on("connect", () => {
310 const wall = {
311 listen(listener) {
312 socket.on("message", (data) => {
313 if (data.uid === UID) {
314 listener(data);
315 }
316 });
317 },
318 send(event, payload) {
319 const data = { event, payload, uid: UID };
320 socket.emit('message', data);
321 },
322 };
323
324 const bridge = createBridge(window, wall);
325 const store = createStore(bridge);
326 const DevTools = createDevTools(window, { bridge, store });
327
328 root = createRoot(document.getElementById('root'));
329 root.render(createElement(DevTools));
330 });
331 socket.on("disconnect", () => {
332 root.unmount();
333 root = null;
334 });
335 ```
336
337 # Local development
338 You can also build and test this package from source.
339
340 ## Prerequisite steps
341 DevTools depends on local versions of several NPM packages<sup>1</sup> also in this workspace. You'll need to either build or download those packages first.
342
343 <sup>1</sup> Note that at this time, an _experimental_ build is required because DevTools depends on the `createRoot` API.
344
345 ### Build from source
346 To build dependencies from source, run the following command from the root of the repository:
347 ```sh
348 yarn build-for-devtools
349 ```
350 ### Download from CI
351 To use the latest build from CI, go to `scripts/release/` and run the following commands:
352 ```sh
353 yarn
354 ./download-experimental-build.js --commit=main
355 ```
356
357 ## Build steps
358 Once the above packages have been built or downloaded, you can watch for changes made to the source code and automatically rebuild by running:
359 ```sh
360 yarn start
361 ```
362
363 To test package changes, refer to the [`react-devtools-shell` README](https://github.com/facebook/react/blob/main/packages/react-devtools-shell/README.md).