feat: expose installHook with settings argument from react-devtools-core/backend (#30987)
Stacked on https://github.com/facebook/react/pull/30986. Previously, we would call `installHook` at a top level of the JavaScript module. Because of this, having `require` statement for `react-devtools-core` package was enough to initialize the React DevTools global hook on the `window`. Now, the Hook can actually receive an argument - initial user settings for console patching. We expose this as a function `initialize`, which can be used by third parties (including React Native) to provide the persisted settings. The README was also updated to reflect the changes.
Ruslan Lesiutin committed
Sep 19, 2024 at 13:55 UTC
a86afe8e560f452a9df5ceb4893d9423e5840800
2 files changed
+62
-23
packages/react-devtools-core/README.md
+29
-18
@@ -14,35 +14,46 @@ If you are building a non-browser-based React renderer, you can use the backend
14
15
```js
16
if (process.env.NODE_ENV !== 'production') {
17
- const { connectToDevTools } = require("react-devtools-core");
17
+ const { initialize, connectToDevTools } = require("react-devtools-core");
18
19
+ initialize(settings);
20
// Must be called before packages like react or react-native are imported
20
- connectToDevTools({
21
- ...config
22
- });
21
+ connectToDevTools({...config});
22
}
23
```
24
25
> **NOTE** that this API (`connectToDevTools`) must be (1) run in the same context as React and (2) must be called before React packages are imported (e.g. `react`, `react-dom`, `react-native`).
26
27
+### `initialize` arguments
28
+| Argument | Description |
29
+|------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
30
+| `settings` | Optional. If not specified, or received as null, then default settings are used. Can be plain object or a Promise that resolves with the [plain settings object](#Settings). If Promise rejects, the console will not be patched and some console features from React DevTools will not work. |
31
+
32
+#### `Settings`
33
+| Spec | Default value |
34
+|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
35
+| <pre>{<br> appendComponentStack: boolean,<br> breakOnConsoleErrors: boolean,<br> showInlineWarningsAndErrors: boolean,<br> hideConsoleLogsInStrictMode: boolean<br>}</pre> | <pre>{<br> appendComponentStack: true,<br> breakOnConsoleErrors: false,<br> showInlineWarningsAndErrors: true,<br> hideConsoleLogsInStrictMode: false<br>}</pre> |
36
+
37
### `connectToDevTools` options
29
-| Prop | Default | Description |
30
-|---|---|---|
31
-| `host` | `"localhost"` | Socket connection to frontend should use this host. |
32
-| `isAppActive` | | (Optional) function that returns true/false, telling DevTools when it's ready to connect to React. |
33
-| `port` | `8097` | Socket connection to frontend should use this port. |
34
-| `resolveRNStyle` | | (Optional) function that accepts a key (number) and returns a style (object); used by React Native. |
35
-| `retryConnectionDelay` | `200` | Delay (ms) to wait between retrying a failed Websocket connection |
36
-| `useHttps` | `false` | Socket connection to frontend should use secure protocol (wss). |
37
-| `websocket` | | Custom `WebSocket` connection to frontend; overrides `host` and `port` settings. |
38
+| Prop | Default | Description |
39
+|------------------------|---------------|---------------------------------------------------------------------------------------------------------------------------|
40
+| `host` | `"localhost"` | Socket connection to frontend should use this host. |
41
+| `isAppActive` | | (Optional) function that returns true/false, telling DevTools when it's ready to connect to React. |
42
+| `port` | `8097` | Socket connection to frontend should use this port. |
43
+| `resolveRNStyle` | | (Optional) function that accepts a key (number) and returns a style (object); used by React Native. |
44
+| `retryConnectionDelay` | `200` | Delay (ms) to wait between retrying a failed Websocket connection |
45
+| `useHttps` | `false` | Socket connection to frontend should use secure protocol (wss). |
46
+| `websocket` | | Custom `WebSocket` connection to frontend; overrides `host` and `port` settings. |
47
+| `onSettingsUpdated` | | A callback that will be called when the user updates the settings in the UI. You can use it for persisting user settings. | |
48
49
50
### `connectWithCustomMessagingProtocol` options
41
-| Prop | Description |
42
-|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
43
-| `onSubscribe` | Function, which receives listener (function, with a single argument) as an argument. Called when backend subscribes to messages from the other end (frontend). |
44
-| `onUnsubscribe` | Function, which receives listener (function) as an argument. Called when backend unsubscribes to messages from the other end (frontend). |
45
-| `onMessage` | Function, which receives 2 arguments: event (string) and payload (any). Called when backend emits a message, which should be sent to the frontend. |
51
+| Prop | Description |
52
+|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
53
+| `onSubscribe` | Function, which receives listener (function, with a single argument) as an argument. Called when backend subscribes to messages from the other end (frontend). |
54
+| `onUnsubscribe` | Function, which receives listener (function) as an argument. Called when backend unsubscribes to messages from the other end (frontend). |
55
+| `onMessage` | Function, which receives 2 arguments: event (string) and payload (any). Called when backend emits a message, which should be sent to the frontend. |
56
+| `onSettingsUpdated` | A callback that will be called when the user updates the settings in the UI. You can use it for persisting user settings. |
57
58
Unlike `connectToDevTools`, `connectWithCustomMessagingProtocol` returns a callback, which can be used for unsubscribing the backend from the global DevTools hook.
59
packages/react-devtools-core/src/backend.js
+33
-5
@@ -20,7 +20,10 @@ import type {
20
ComponentFilter,
21
Wall,
22
} from 'react-devtools-shared/src/frontend/types';
23
-import type {DevToolsHook} from 'react-devtools-shared/src/backend/types';
23
+import type {
24
+ DevToolsHook,
25
+ DevToolsHookSettings,
26
+} from 'react-devtools-shared/src/backend/types';
27
import type {ResolveNativeStyle} from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
28
29
type ConnectOptions = {
@@ -32,12 +35,9 @@ type ConnectOptions = {
35
retryConnectionDelay?: number,
36
isAppActive?: () => boolean,
37
websocket?: ?WebSocket,
38
+ onSettingsUpdated?: (settings: $ReadOnly<DevToolsHookSettings>) => void,
39
};
40
37
-installHook(window);
38
-
39
-const hook: ?DevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
40
-
41
let savedComponentFilters: Array<ComponentFilter> =
42
getDefaultComponentFilters();
43
@@ -52,11 +52,21 @@ function debug(methodName: string, ...args: Array<mixed>) {
52
}
53
}
54
55
+export function initialize(
56
+ maybeSettingsOrSettingsPromise?:
57
+ | DevToolsHookSettings
58
+ | Promise<DevToolsHookSettings>,
59
+) {
60
+ installHook(window, maybeSettingsOrSettingsPromise);
61
+}
62
+
63
export function connectToDevTools(options: ?ConnectOptions) {
64
+ const hook: ?DevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
65
if (hook == null) {
66
// DevTools didn't get injected into this page (maybe b'c of the contentType).
67
return;
68
}
69
+
70
const {
71
host = 'localhost',
72
nativeStyleEditorValidAttributes,
@@ -66,6 +76,7 @@ export function connectToDevTools(options: ?ConnectOptions) {
76
resolveRNStyle = (null: $FlowFixMe),
77
retryConnectionDelay = 2000,
78
isAppActive = () => true,
79
+ onSettingsUpdated,
80
} = options || {};
81
82
const protocol = useHttps ? 'wss' : 'ws';
@@ -160,7 +171,14 @@ export function connectToDevTools(options: ?ConnectOptions) {
171
// TODO (npm-packages) Warn if "isBackendStorageAPISupported"
172
// $FlowFixMe[incompatible-call] found when upgrading Flow
173
const agent = new Agent(bridge);
174
+ if (onSettingsUpdated != null) {
175
+ agent.addListener('updateHookSettings', onSettingsUpdated);
176
+ }
177
agent.addListener('shutdown', () => {
178
+ if (onSettingsUpdated != null) {
179
+ agent.removeListener('updateHookSettings', onSettingsUpdated);
180
+ }
181
+
182
// If we received 'shutdown' from `agent`, we assume the `bridge` is already shutting down,
183
// and that caused the 'shutdown' event on the `agent`, so we don't need to call `bridge.shutdown()` here.
184
hook.emit('shutdown');
@@ -290,6 +308,7 @@ type ConnectWithCustomMessagingOptions = {
308
onMessage: (event: string, payload: any) => void,
309
nativeStyleEditorValidAttributes?: $ReadOnlyArray<string>,
310
resolveRNStyle?: ResolveNativeStyle,
311
+ onSettingsUpdated?: (settings: $ReadOnly<DevToolsHookSettings>) => void,
312
};
313
314
export function connectWithCustomMessagingProtocol({
@@ -298,7 +317,9 @@ export function connectWithCustomMessagingProtocol({
317
onMessage,
318
nativeStyleEditorValidAttributes,
319
resolveRNStyle,
320
+ onSettingsUpdated,
321
}: ConnectWithCustomMessagingOptions): Function {
322
+ const hook: ?DevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
323
if (hook == null) {
324
// DevTools didn't get injected into this page (maybe b'c of the contentType).
325
return;
@@ -334,7 +355,14 @@ export function connectWithCustomMessagingProtocol({
355
}
356
357
const agent = new Agent(bridge);
358
+ if (onSettingsUpdated != null) {
359
+ agent.addListener('updateHookSettings', onSettingsUpdated);
360
+ }
361
agent.addListener('shutdown', () => {
362
+ if (onSettingsUpdated != null) {
363
+ agent.removeListener('updateHookSettings', onSettingsUpdated);
364
+ }
365
+
366
// If we received 'shutdown' from `agent`, we assume the `bridge` is already shutting down,
367
// and that caused the 'shutdown' event on the `agent`, so we don't need to call `bridge.shutdown()` here.
368
hook.emit('shutdown');