Files
react/packages/react-devtools-inline/README.md

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

364 lines
12 KiB
Markdown
Raw Normal View History

# `react-devtools-inline`
2022-03-16 08:37:10 -07:00
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).
2022-03-16 08:37:10 -07:00
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**.
2022-03-16 08:37:10 -07:00
---
> **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`.
2022-03-16 08:37:10 -07:00
---
# Usage
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>).
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:
1. Frontend (DevTools interface) initialized in the main `window`.
1. Backend initialized in an `iframe`.
1. Backend activated.
<sup>1</sup> Sandboxed iframes are supported.
2022-03-16 08:37:10 -07:00
# Backend APIs
### `initialize(windowOrGlobal)`
Installs the global hook on the window/global object. This hook is how React and DevTools communicate.
2022-03-16 08:37:10 -07:00
> **This method must be called before React is loaded.** (This includes `import`/`require` statements and `<script>` tags that include React.)
### `activate(windowOrGlobal)`
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.
2022-03-16 08:37:10 -07:00
### Example
```js
import { activate, initialize } from 'react-devtools-inline/backend';
// This should be the iframe the React application is running in.
const iframe = document.getElementById(frameID);
const contentWindow = iframe.contentWindow;
// Call this before importing React (or any other packages that might import React).
initialize(contentWindow);
// Initialize the frontend...
// Call this only once the frontend has been initialized.
activate(contentWindow);
```
2022-03-16 08:37:10 -07:00
# Frontend APIs
2022-03-16 08:37:10 -07:00
### `initialize(windowOrGlobal)`
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.
2022-03-16 08:37:10 -07:00
> 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`.**
2022-03-16 08:37:10 -07:00
### Example
```js
import { initialize } from 'react-devtools-inline/frontend';
// This should be the iframe the backend hook has been installed in.
const iframe = document.getElementById(frameID);
const contentWindow = iframe.contentWindow;
// This returns a React component that can be rendered into your app.
// e.g. render(<DevTools {...props} />);
const DevTools = initialize(contentWindow);
```
2022-03-16 08:37:10 -07:00
# Advanced examples
Add named hooks support to react-devtools-inline (#22263) This commit builds on PR #22260 and makes the following changes: * Adds a DevTools feature flag for named hooks support. (This allows us to disable it entirely for a build via feature flag.) * Adds a new Suspense cache for dynamically imported modules. (This allows a component to suspend while importing an external code chunk– like the hook names parsing code). * DevTools supports a hookNamesModuleLoaderFunction param to import the hook names module. I wish this could be handles as part of the react-devtools-shared package, but I'm not sure how to configure Webpack (4) to serve the chunk from react-devtools-inline. This seemed like a reasonable workaround. The PR also contains an additional unrelated change: * Removes pre-fetch optimization (added in DevTools: Improve named hooks network caching #22198). This optimization was mostly only important for cases where sources needed to be re-downloaded, something which we can now avoid in most cases¹ thanks to using cached responses already loaded by the page. (I tested this locally on Facebook and this change has no negative performance impact. There is still some overhead from serializing the JS through the Bridge but that's constant between the two approaches.) ¹ The case where we don't benefit from cached responses is when DevTools are opened after the page has already loaded certain scripts. This seems uncommon enough that I don't think it justified the added complexity of prefetching.
2021-09-09 15:25:26 -04:00
### Supporting named hooks
2021-11-15 16:59:35 +01:00
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.
Add named hooks support to react-devtools-inline (#22263) This commit builds on PR #22260 and makes the following changes: * Adds a DevTools feature flag for named hooks support. (This allows us to disable it entirely for a build via feature flag.) * Adds a new Suspense cache for dynamically imported modules. (This allows a component to suspend while importing an external code chunk– like the hook names parsing code). * DevTools supports a hookNamesModuleLoaderFunction param to import the hook names module. I wish this could be handles as part of the react-devtools-shared package, but I'm not sure how to configure Webpack (4) to serve the chunk from react-devtools-inline. This seemed like a reasonable workaround. The PR also contains an additional unrelated change: * Removes pre-fetch optimization (added in DevTools: Improve named hooks network caching #22198). This optimization was mostly only important for cases where sources needed to be re-downloaded, something which we can now avoid in most cases¹ thanks to using cached responses already loaded by the page. (I tested this locally on Facebook and this change has no negative performance impact. There is still some overhead from serializing the JS through the Bridge but that's constant between the two approaches.) ¹ The case where we don't benefit from cached responses is when DevTools are opened after the page has already loaded certain scripts. This seems uncommon enough that I don't think it justified the added complexity of prefetching.
2021-09-09 15:25:26 -04:00
To configure this package to support this functionality, you'll need to provide a prop that dynamically imports the extra functionality:
```js
// Follow code examples above to configure the backend and frontend.
// When rendering DevTools, the important part is to pass a 'hookNamesModuleLoaderFunction' prop.
const hookNamesModuleLoaderFunction = () => import('react-devtools-inline/hookNames');
// Render:
<DevTools
hookNamesModuleLoaderFunction={hookNamesModuleLoaderFunction}
{...otherProps}
/>;
```
### Configuring a same-origin `iframe`
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.
```js
import {
activate as activateBackend,
initialize as initializeBackend
} from 'react-devtools-inline/backend';
import { initialize as initializeFrontend } from 'react-devtools-inline/frontend';
// The React app you want to inspect with DevTools is running within this iframe:
const iframe = document.getElementById('target');
const { contentWindow } = iframe;
// Installs the global hook into the iframe.
// This must be called before React is loaded into that frame.
initializeBackend(contentWindow);
// Initialize DevTools UI to listen to the hook we just installed.
// This returns a React component we can render anywhere in the parent window.
// This also must be called before React is loaded into the iframe
const DevTools = initializeFrontend(contentWindow);
// React application can be injected into <iframe> at any time now...
// Note that this would need to be done via <script> tag injection,
// as setting the src of the <iframe> would load a new page (without the injected backend).
// <DevTools /> interface can be rendered in the parent window at any time now...
Move createRoot/hydrateRoot to react-dom/client (#23385) * Move createRoot/hydrateRoot to /client We want these APIs ideally to be imported separately from things you might use in arbitrary components (like flushSync). Those other methods are "isomorphic" to how the ReactDOM tree is rendered. Similar to hooks. E.g. importing flushSync into a component that only uses it on the client should ideally not also pull in the entry client implementation on the server. This also creates a nicer parity with /server where the roots are in a separate entry point. Unfortunately, I can't quite do this yet because we have some legacy APIs that we plan on removing (like findDOMNode) and we also haven't implemented flushSync using a flag like startTransition does yet. Another problem is that we currently encourage these APIs to be aliased by /profiling (or unstable_testing). In the future you don't have to alias them because you can just change your roots to just import those APIs and they'll still work with the isomorphic forms. Although we might also just use export conditions for them. For that all to work, I went with a different strategy for now where the real API is in / but it comes with a warning if you use it. If you instead import /client it disables the warning in a wrapper. That means that if you alias / then import /client that will inturn import the alias and it'll just work. In a future breaking changes (likely when we switch to ESM) we can just remove createRoot/hydrateRoot from / and move away from the aliasing strategy. * Update tests to import from react-dom/client * Fix fixtures * Update warnings * Add test for the warning * Update devtools * Change order of react-dom, react-dom/client alias I think the order matters here. The first one takes precedence. * Require react-dom through client so it can be aliased Co-authored-by: Andrew Clark <git@andrewclark.io>
2022-03-01 00:13:28 -05:00
// Be sure to use ReactDOMClient.createRoot() to render this component.
// Let the backend know the frontend is ready and listening.
activateBackend(contentWindow);
```
### Configuring a sandboxed `iframe`
Sandboxed `iframe`s are also supported but require more complex initialization.
**`iframe.html`**
```js
import { activate, initialize } from "react-devtools-inline/backend";
2020-06-15 19:59:44 -04:00
// The DevTools hook needs to be installed before React is even required!
// The safest way to do this is probably to install it in a separate script tag.
initialize(window);
// Wait for the frontend to let us know that it's ready.
function onMessage({ data }) {
switch (data.type) {
case "activate-backend":
window.removeEventListener("message", onMessage);
activate(window);
break;
default:
break;
}
}
window.addEventListener("message", onMessage);
```
**`main-window.html`**
```js
import { initialize } from "react-devtools-inline/frontend";
const iframe = document.getElementById("target");
const { contentWindow } = iframe;
// Initialize DevTools UI to listen to the iframe.
// This returns a React component we can render anywhere in the main window.
Move createRoot/hydrateRoot to react-dom/client (#23385) * Move createRoot/hydrateRoot to /client We want these APIs ideally to be imported separately from things you might use in arbitrary components (like flushSync). Those other methods are "isomorphic" to how the ReactDOM tree is rendered. Similar to hooks. E.g. importing flushSync into a component that only uses it on the client should ideally not also pull in the entry client implementation on the server. This also creates a nicer parity with /server where the roots are in a separate entry point. Unfortunately, I can't quite do this yet because we have some legacy APIs that we plan on removing (like findDOMNode) and we also haven't implemented flushSync using a flag like startTransition does yet. Another problem is that we currently encourage these APIs to be aliased by /profiling (or unstable_testing). In the future you don't have to alias them because you can just change your roots to just import those APIs and they'll still work with the isomorphic forms. Although we might also just use export conditions for them. For that all to work, I went with a different strategy for now where the real API is in / but it comes with a warning if you use it. If you instead import /client it disables the warning in a wrapper. That means that if you alias / then import /client that will inturn import the alias and it'll just work. In a future breaking changes (likely when we switch to ESM) we can just remove createRoot/hydrateRoot from / and move away from the aliasing strategy. * Update tests to import from react-dom/client * Fix fixtures * Update warnings * Add test for the warning * Update devtools * Change order of react-dom, react-dom/client alias I think the order matters here. The first one takes precedence. * Require react-dom through client so it can be aliased Co-authored-by: Andrew Clark <git@andrewclark.io>
2022-03-01 00:13:28 -05:00
// Be sure to use ReactDOMClient.createRoot() to render this component.
const DevTools = initialize(contentWindow);
// Let the backend know to initialize itself.
// We can't do this directly because the iframe is sandboxed.
// Only initialize the backend once the DevTools frontend has been initialized.
iframe.onload = () => {
contentWindow.postMessage(
{
type: "activate-backend"
},
"*"
);
};
```
2022-03-16 08:37:10 -07:00
### Advanced: Custom "wall"
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).
```js
import {
activate as activateBackend,
createBridge as createBackendBridge,
initialize as initializeBackend,
} from 'react-devtools-inline/backend';
import {
createBridge as createFrontendBridge,
createStore,
initialize as createDevTools,
} from 'react-devtools-inline/frontend';
// DevTools uses "message" events and window.postMessage() by default,
// but we can override this behavior by creating a custom "Wall" object.
// For example...
const wall = {
_listeners: [],
listen(listener) {
wall._listeners.push(listener);
},
send(event, payload) {
wall._listeners.forEach(listener => listener({event, payload}));
},
};
// Initialize the DevTools backend before importing React (or any other packages that might import React).
initializeBackend(contentWindow);
// Prepare DevTools for rendering.
// To use the custom Wall we've created, we need to also create our own "Bridge" and "Store" objects.
const bridge = createFrontendBridge(contentWindow, wall);
const store = createStore(bridge);
const DevTools = createDevTools(contentWindow, { bridge, store });
// You can render DevTools now:
const root = createRoot(container);
root.render(<DevTools {...otherProps} />);
// Lastly, let the DevTools backend know that the frontend is ready.
// To use the custom Wall we've created, we need to also pass in the "Bridge".
activateBackend(contentWindow, {
bridge: createBackendBridge(contentWindow, wall),
});
```
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:
```js
const uid = "some-unique-string-shared-between-both-pieces";
const wall = {
listen(listener) {
window.addEventListener("message", (event) => {
if (event.data.uid === uid) {
listener(event.data);
}
});
},
send(event, payload) {
window.postMessage({ event, payload, uid }, "*");
},
};
```
2022-03-16 08:37:10 -07:00
### Advanced: Node + browser
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.
##### Sample Node backend
```js
const {
activate,
createBridge,
initialize,
} = require('react-devtools-inline/backend');
const { createServer } = require('http');
const SocketIO = require('socket.io');
const server = createServer();
const socket = SocketIO(server, {
cors: {
origin: "*",
methods: ["GET", "POST"],
allowedHeaders: [],
credentials: true
}
});
socket.on('connection', client => {
const wall = {
listen(listener) {
client.on('message', data => {
if (data.uid === UID) {
listener(data);
}
});
},
send(event, payload) {
const data = {event, payload, uid: UID};
client.emit('message', data);
},
};
const bridge = createBridge(global, wall);
client.on('disconnect', () => {
bridge.shutdown();
});
activate(global, { bridge });
});
socket.listen(PORT);
```
##### Sample Web frontend
```js
import { createElement } from 'react';
import { createRoot } from 'react-dom/client';
import {
createBridge,
createStore,
initialize as createDevTools,
} from 'react-devtools-inline/frontend';
import { io } from "socket.io-client";
let root = null;
const socket = io(`http://${HOST}:${PORT}`);
socket.on("connect", () => {
const wall = {
listen(listener) {
socket.on("message", (data) => {
if (data.uid === UID) {
listener(data);
}
});
},
send(event, payload) {
const data = { event, payload, uid: UID };
socket.emit('message', data);
},
};
const bridge = createBridge(window, wall);
const store = createStore(bridge);
const DevTools = createDevTools(window, { bridge, store });
root = createRoot(document.getElementById('root'));
root.render(createElement(DevTools));
});
socket.on("disconnect", () => {
root.unmount();
root = null;
});
```
# Local development
You can also build and test this package from source.
2022-03-16 08:37:10 -07:00
## Prerequisite steps
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.
<sup>1</sup> Note that at this time, an _experimental_ build is required because DevTools depends on the `createRoot` API.
2022-03-16 08:37:10 -07:00
### Build from source
To build dependencies from source, run the following command from the root of the repository:
```sh
yarn build-for-devtools
```
2022-03-16 08:37:10 -07:00
### Download from CI
To use the latest build from CI, go to `scripts/release/` and run the following commands:
```sh
yarn
./download-experimental-build.js --commit=main
```
2022-03-16 08:37:10 -07:00
## Build steps
Once the above packages have been built or downloaded, you can watch for changes made to the source code and automatically rebuild by running:
```sh
yarn start
```
To test package changes, refer to the [`react-devtools-shell` README](https://github.com/facebook/react/blob/main/packages/react-devtools-shell/README.md).