@samitouri / QOS-React-1 / commits / e33acfd67f

refactor[react-devtools]: propagate settings from global hook object to frontend (#30610)

Stacked on https://github.com/facebook/react/pull/30597 and whats under it. See [this commit](https://github.com/facebook/react/pull/30610/commits/59b4efa72377bf62f5ec8c0e32e56902cf73fbd7). With this change, the initial values for console patching settings are propagated from hook (which is the source of truth now, because of https://github.com/facebook/react/pull/30596) to the UI. Instead of reading from `localStorage` the frontend is now requesting it from the hook. This happens when settings modal is rendered, and wrapped in a transition. Also, this is happening even if settings modal is not opened yet, so we have enough time to fetch this data without displaying loader or similar UI.

Ruslan Lesiutin committed Sep 18, 2024 at 18:19 UTC e33acfd67f0003272a9aec7a0725d19a429f2460
11 files changed +145 -99
packages/react-devtools-core/src/backend.js
+2 -2
@@ -162,7 +162,7 @@ export function connectToDevTools(options: ?ConnectOptions) {
162 );
163
164 if (devToolsSettingsManager != null && bridge != null) {
165 - bridge.addListener('updateConsolePatchSettings', consolePatchSettings =>
165 + bridge.addListener('updateHookSettings', consolePatchSettings =>
166 cacheConsolePatchSettings(
167 devToolsSettingsManager,
168 consolePatchSettings,
@@ -368,7 +368,7 @@ export function connectWithCustomMessagingProtocol({
368 );
369
370 if (settingsManager != null) {
371 - bridge.addListener('updateConsolePatchSettings', consolePatchSettings =>
371 + bridge.addListener('updateHookSettings', consolePatchSettings =>
372 cacheConsolePatchSettings(settingsManager, consolePatchSettings),
373 );
374 }
packages/react-devtools-core/src/cachedSettings.js
+1 -1
@@ -66,7 +66,7 @@ function parseConsolePatchSettings(
66
67 export function cacheConsolePatchSettings(
68 devToolsSettingsManager: DevToolsSettingsManager,
69 - value: ConsolePatchSettings,
69 + value: $ReadOnly<ConsolePatchSettings>,
70 ): void {
71 if (devToolsSettingsManager.setConsolePatchSettings == null) {
72 return;
packages/react-devtools-shared/src/backend/agent.js
+23 -14
@@ -150,6 +150,7 @@ export default class Agent extends EventEmitter<{
150 disableTraceUpdates: [],
151 getIfHasUnsupportedRendererVersion: [],
152 updateHookSettings: [DevToolsHookSettings],
153 + getHookSettings: [],
154 }> {
155 _bridge: BackendBridge;
156 _isProfiling: boolean = false;
@@ -213,10 +214,10 @@ export default class Agent extends EventEmitter<{
214 this.syncSelectionFromBuiltinElementsPanel,
215 );
216 bridge.addListener('shutdown', this.shutdown);
216 - bridge.addListener(
217 - 'updateConsolePatchSettings',
218 - this.updateConsolePatchSettings,
219 - );
217 +
218 + bridge.addListener('updateHookSettings', this.updateHookSettings);
219 + bridge.addListener('getHookSettings', this.getHookSettings);
220 +
221 bridge.addListener('updateComponentFilters', this.updateComponentFilters);
222 bridge.addListener('getEnvironmentNames', this.getEnvironmentNames);
223 bridge.addListener(
@@ -802,18 +803,26 @@ export default class Agent extends EventEmitter<{
803 }
804 };
805
805 - updateConsolePatchSettings: (
806 - settings: $ReadOnly<DevToolsHookSettings>,
807 - ) => void = settings => {
808 - // Propagate the settings, so Backend can subscribe to it and modify hook
809 - this.emit('updateHookSettings', {
810 - appendComponentStack: settings.appendComponentStack,
811 - breakOnConsoleErrors: settings.breakOnConsoleErrors,
812 - showInlineWarningsAndErrors: settings.showInlineWarningsAndErrors,
813 - hideConsoleLogsInStrictMode: settings.hideConsoleLogsInStrictMode,
814 - });
806 + updateHookSettings: (settings: $ReadOnly<DevToolsHookSettings>) => void =
807 + settings => {
808 + // Propagate the settings, so Backend can subscribe to it and modify hook
809 + this.emit('updateHookSettings', {
810 + appendComponentStack: settings.appendComponentStack,
811 + breakOnConsoleErrors: settings.breakOnConsoleErrors,
812 + showInlineWarningsAndErrors: settings.showInlineWarningsAndErrors,
813 + hideConsoleLogsInStrictMode: settings.hideConsoleLogsInStrictMode,
814 + });
815 + };
816 +
817 + getHookSettings: () => void = () => {
818 + this.emit('getHookSettings');
819 };
820
821 + onHookSettings: (settings: $ReadOnly<DevToolsHookSettings>) => void =
822 + settings => {
823 + this._bridge.send('hookSettings', settings);
824 + };
825 +
826 updateComponentFilters: (componentFilters: Array<ComponentFilter>) => void =
827 componentFilters => {
828 for (const rendererIDString in this._rendererInterfaces) {
packages/react-devtools-shared/src/backend/index.js
+7
@@ -54,6 +54,7 @@ export function initBackend(
54 hook.sub('fastRefreshScheduled', agent.onFastRefreshScheduled),
55 hook.sub('operations', agent.onHookOperations),
56 hook.sub('traceUpdates', agent.onTraceUpdates),
57 + hook.sub('settingsInitialized', agent.onHookSettings),
58
59 // TODO Add additional subscriptions required for profiling mode
60 ];
@@ -87,6 +88,12 @@ export function initBackend(
88 hook.settings = settings;
89 });
90
91 + agent.addListener('getHookSettings', () => {
92 + if (hook.settings != null) {
93 + agent.onHookSettings(hook.settings);
94 + }
95 + });
96 +
97 return () => {
98 subs.forEach(fn => fn());
99 };
packages/react-devtools-shared/src/bridge.js
+5 -1
@@ -207,6 +207,8 @@ export type BackendEvents = {
207 {isSupported: boolean, validAttributes: ?$ReadOnlyArray<string>},
208 ],
209 NativeStyleEditor_styleAndLayout: [StyleAndLayoutPayload],
210 +
211 + hookSettings: [$ReadOnly<DevToolsHookSettings>],
212 };
213
214 type FrontendEvents = {
@@ -241,7 +243,7 @@ type FrontendEvents = {
243 storeAsGlobal: [StoreAsGlobalParams],
244 updateComponentFilters: [Array<ComponentFilter>],
245 getEnvironmentNames: [],
244 - updateConsolePatchSettings: [DevToolsHookSettings],
246 + updateHookSettings: [$ReadOnly<DevToolsHookSettings>],
247 viewAttributeSource: [ViewAttributeSourceParams],
248 viewElementSource: [ElementAndRendererID],
249
@@ -267,6 +269,8 @@ type FrontendEvents = {
269
270 resumeElementPolling: [],
271 pauseElementPolling: [],
272 +
273 + getHookSettings: [],
274 };
275
276 class Bridge<
packages/react-devtools-shared/src/devtools/store.js
+25
@@ -49,6 +49,7 @@ import type {
49 BridgeProtocol,
50 } from 'react-devtools-shared/src/bridge';
51 import UnsupportedBridgeOperationError from 'react-devtools-shared/src/UnsupportedBridgeOperationError';
52 +import type {DevToolsHookSettings} from '../backend/types';
53
54 const debug = (methodName: string, ...args: Array<string>) => {
55 if (__DEBUG__) {
@@ -94,6 +95,7 @@ export default class Store extends EventEmitter<{
95 collapseNodesByDefault: [],
96 componentFilters: [],
97 error: [Error],
98 + hookSettings: [$ReadOnly<DevToolsHookSettings>],
99 mutated: [[Array<number>, Map<number, number>]],
100 recordChangeDescriptions: [],
101 roots: [],
@@ -192,6 +194,7 @@ export default class Store extends EventEmitter<{
194 _weightAcrossRoots: number = 0;
195
196 _shouldCheckBridgeProtocolCompatibility: boolean = false;
197 + _hookSettings: $ReadOnly<DevToolsHookSettings> | null = null;
198
199 constructor(bridge: FrontendBridge, config?: Config) {
200 super();
@@ -270,6 +273,7 @@ export default class Store extends EventEmitter<{
273
274 bridge.addListener('backendVersion', this.onBridgeBackendVersion);
275 bridge.addListener('saveToClipboard', this.onSaveToClipboard);
276 + bridge.addListener('hookSettings', this.onHookSettings);
277 bridge.addListener('backendInitialized', this.onBackendInitialized);
278 }
279
@@ -1501,8 +1505,29 @@ export default class Store extends EventEmitter<{
1505
1506 this._bridge.send('getBackendVersion');
1507 this._bridge.send('getIfHasUnsupportedRendererVersion');
1508 + this._bridge.send('getHookSettings'); // Warm up cached hook settings
1509 };
1510
1511 + getHookSettings: () => void = () => {
1512 + if (this._hookSettings != null) {
1513 + this.emit('hookSettings', this._hookSettings);
1514 + } else {
1515 + this._bridge.send('getHookSettings');
1516 + }
1517 + };
1518 +
1519 + updateHookSettings: (settings: $ReadOnly<DevToolsHookSettings>) => void =
1520 + settings => {
1521 + this._hookSettings = settings;
1522 + this._bridge.send('updateHookSettings', settings);
1523 + };
1524 +
1525 + onHookSettings: (settings: $ReadOnly<DevToolsHookSettings>) => void =
1526 + settings => {
1527 + this._hookSettings = settings;
1528 + this.emit('hookSettings', settings);
1529 + };
1530 +
1531 // The Store should never throw an Error without also emitting an event.
1532 // Otherwise Store errors will be invisible to users,
1533 // but the downstream errors they cause will be reported as bugs.
packages/react-devtools-shared/src/devtools/views/Settings/DebuggingSettings.js
+37 -10
@@ -8,22 +8,49 @@
8 */
9
10 import * as React from 'react';
11 -import {useContext} from 'react';
12 -import {SettingsContext} from './SettingsContext';
11 +import {use, useState, useEffect} from 'react';
12 +
13 +import type {DevToolsHookSettings} from 'react-devtools-shared/src/backend/types';
14 +import type Store from 'react-devtools-shared/src/devtools/store';
15
16 import styles from './SettingsShared.css';
17
16 -export default function DebuggingSettings(_: {}): React.Node {
17 - const {
18 +type Props = {
19 + hookSettings: Promise<$ReadOnly<DevToolsHookSettings>>,
20 + store: Store,
21 +};
22 +
23 +export default function DebuggingSettings({
24 + hookSettings,
25 + store,
26 +}: Props): React.Node {
27 + const usedHookSettings = use(hookSettings);
28 +
29 + const [appendComponentStack, setAppendComponentStack] = useState(
30 + usedHookSettings.appendComponentStack,
31 + );
32 + const [breakOnConsoleErrors, setBreakOnConsoleErrors] = useState(
33 + usedHookSettings.breakOnConsoleErrors,
34 + );
35 + const [hideConsoleLogsInStrictMode, setHideConsoleLogsInStrictMode] =
36 + useState(usedHookSettings.hideConsoleLogsInStrictMode);
37 + const [showInlineWarningsAndErrors, setShowInlineWarningsAndErrors] =
38 + useState(usedHookSettings.showInlineWarningsAndErrors);
39 +
40 + useEffect(() => {
41 + store.updateHookSettings({
42 + appendComponentStack,
43 + breakOnConsoleErrors,
44 + showInlineWarningsAndErrors,
45 + hideConsoleLogsInStrictMode,
46 + });
47 + }, [
48 + store,
49 appendComponentStack,
50 breakOnConsoleErrors,
20 - hideConsoleLogsInStrictMode,
21 - setAppendComponentStack,
22 - setBreakOnConsoleErrors,
23 - setShowInlineWarningsAndErrors,
51 showInlineWarningsAndErrors,
25 - setHideConsoleLogsInStrictMode,
26 - } = useContext(SettingsContext);
52 + hideConsoleLogsInStrictMode,
53 + ]);
54
55 return (
56 <div className={styles.Settings}>
packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js
-55
@@ -20,11 +20,7 @@ import {
20 import {
21 LOCAL_STORAGE_BROWSER_THEME,
22 LOCAL_STORAGE_PARSE_HOOK_NAMES_KEY,
23 - LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
24 - LOCAL_STORAGE_SHOULD_APPEND_COMPONENT_STACK_KEY,
23 LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
26 - LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
27 - LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE,
24 } from 'react-devtools-shared/src/constants';
25 import {
26 COMFORTABLE_LINE_HEIGHT,
@@ -118,30 +114,10 @@ function SettingsContextController({
114 LOCAL_STORAGE_BROWSER_THEME,
115 'auto',
116 );
121 - const [appendComponentStack, setAppendComponentStack] =
122 - useLocalStorageWithLog<boolean>(
123 - LOCAL_STORAGE_SHOULD_APPEND_COMPONENT_STACK_KEY,
124 - true,
125 - );
126 - const [breakOnConsoleErrors, setBreakOnConsoleErrors] =
127 - useLocalStorageWithLog<boolean>(
128 - LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
129 - false,
130 - );
117 const [parseHookNames, setParseHookNames] = useLocalStorageWithLog<boolean>(
118 LOCAL_STORAGE_PARSE_HOOK_NAMES_KEY,
119 false,
120 );
135 - const [hideConsoleLogsInStrictMode, setHideConsoleLogsInStrictMode] =
136 - useLocalStorageWithLog<boolean>(
137 - LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE,
138 - false,
139 - );
140 - const [showInlineWarningsAndErrors, setShowInlineWarningsAndErrors] =
141 - useLocalStorageWithLog<boolean>(
142 - LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
143 - true,
144 - );
121 const [traceUpdatesEnabled, setTraceUpdatesEnabled] =
122 useLocalStorageWithLog<boolean>(
123 LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
@@ -196,64 +172,33 @@ function SettingsContextController({
172 }
173 }, [browserTheme, theme, documentElements]);
174
199 - useEffect(() => {
200 - bridge.send('updateConsolePatchSettings', {
201 - appendComponentStack,
202 - breakOnConsoleErrors,
203 - showInlineWarningsAndErrors,
204 - hideConsoleLogsInStrictMode,
205 - });
206 - }, [
207 - bridge,
208 - appendComponentStack,
209 - breakOnConsoleErrors,
210 - showInlineWarningsAndErrors,
211 - hideConsoleLogsInStrictMode,
212 - ]);
213 -
175 useEffect(() => {
176 bridge.send('setTraceUpdatesEnabled', traceUpdatesEnabled);
177 }, [bridge, traceUpdatesEnabled]);
178
179 const value = useMemo(
180 () => ({
220 - appendComponentStack,
221 - breakOnConsoleErrors,
181 displayDensity,
182 lineHeight:
183 displayDensity === 'compact'
184 ? COMPACT_LINE_HEIGHT
185 : COMFORTABLE_LINE_HEIGHT,
186 parseHookNames,
228 - setAppendComponentStack,
229 - setBreakOnConsoleErrors,
187 setDisplayDensity,
188 setParseHookNames,
189 setTheme,
190 setTraceUpdatesEnabled,
234 - setShowInlineWarningsAndErrors,
235 - showInlineWarningsAndErrors,
236 - setHideConsoleLogsInStrictMode,
237 - hideConsoleLogsInStrictMode,
191 theme,
192 browserTheme,
193 traceUpdatesEnabled,
194 }),
195 [
243 - appendComponentStack,
244 - breakOnConsoleErrors,
196 displayDensity,
197 parseHookNames,
247 - setAppendComponentStack,
248 - setBreakOnConsoleErrors,
198 setDisplayDensity,
199 setParseHookNames,
200 setTheme,
201 setTraceUpdatesEnabled,
253 - setShowInlineWarningsAndErrors,
254 - showInlineWarningsAndErrors,
255 - setHideConsoleLogsInStrictMode,
256 - hideConsoleLogsInStrictMode,
202 theme,
203 browserTheme,
204 traceUpdatesEnabled,
packages/react-devtools-shared/src/devtools/views/Settings/SettingsModal.js
+10 -7
@@ -26,9 +26,11 @@ import ProfilerSettings from './ProfilerSettings';
26
27 import styles from './SettingsModal.css';
28
29 -type TabID = 'general' | 'components' | 'profiler';
29 +import type Store from 'react-devtools-shared/src/devtools/store';
30
31 -export default function SettingsModal(_: {}): React.Node {
31 +type TabID = 'general' | 'debugging' | 'components' | 'profiler';
32 +
33 +export default function SettingsModal(): React.Node {
34 const {isModalShowing, setIsModalShowing} = useContext(SettingsModalContext);
35 const store = useContext(StoreContext);
36 const {profilerStore} = store;
@@ -54,11 +56,13 @@ export default function SettingsModal(_: {}): React.Node {
56 return null;
57 }
58
57 - return <SettingsModalImpl />;
59 + return <SettingsModalImpl store={store} />;
60 }
61
60 -function SettingsModalImpl(_: {}) {
61 - const {setIsModalShowing, environmentNames} =
62 +type ImplProps = {store: Store};
63 +
64 +function SettingsModalImpl({store}: ImplProps) {
65 + const {setIsModalShowing, environmentNames, hookSettings} =
66 useContext(SettingsModalContext);
67 const dismissModal = useCallback(
68 () => setIsModalShowing(false),
@@ -84,9 +88,8 @@ function SettingsModalImpl(_: {}) {
88 case 'components':
89 view = <ComponentsSettings environmentNames={environmentNames} />;
90 break;
87 - // $FlowFixMe[incompatible-type] is this missing in TabID?
91 case 'debugging':
89 - view = <DebuggingSettings />;
92 + view = <DebuggingSettings hookSettings={hookSettings} store={store} />;
93 break;
94 case 'general':
95 view = <GeneralSettings />;
packages/react-devtools-shared/src/devtools/views/Settings/SettingsModalContext.js
+33 -9
@@ -18,8 +18,11 @@ import {
18 startTransition,
19 } from 'react';
20
21 -import {BridgeContext} from '../context';
21 +import {BridgeContext, StoreContext} from '../context';
22 +
23 import type {FrontendBridge} from '../../../bridge';
24 +import type {DevToolsHookSettings} from '../../../backend/types';
25 +import type Store from '../../store';
26
27 export type DisplayDensity = 'comfortable' | 'compact';
28 export type Theme = 'auto' | 'light' | 'dark';
@@ -28,6 +31,7 @@ type Context = {
31 isModalShowing: boolean,
32 setIsModalShowing: (value: boolean) => void,
33 environmentNames: null | Promise<Array<string>>,
34 + hookSettings: null | Promise<$ReadOnly<DevToolsHookSettings>>,
35 };
36
37 const SettingsModalContext: ReactContext<Context> = createContext<Context>(
@@ -46,27 +50,47 @@ function fetchEnvironmentNames(bridge: FrontendBridge): Promise<Array<string>> {
50 });
51 }
52
53 +function fetchHookSettings(
54 + store: Store,
55 +): Promise<$ReadOnly<DevToolsHookSettings>> {
56 + return new Promise(resolve => {
57 + function onHookSettings(settings: $ReadOnly<DevToolsHookSettings>) {
58 + store.removeListener('hookSettings', onHookSettings);
59 + resolve(settings);
60 + }
61 +
62 + store.addListener('hookSettings', onHookSettings);
63 + store.getHookSettings();
64 + });
65 +}
66 +
67 function SettingsModalContextController({
68 children,
69 }: {
70 children: React$Node,
71 }): React.Node {
72 const bridge = useContext(BridgeContext);
73 + const store = useContext(StoreContext);
74
56 - const setIsModalShowing: boolean => void = useCallback((value: boolean) => {
57 - startTransition(() => {
58 - setContext({
59 - isModalShowing: value,
60 - setIsModalShowing,
61 - environmentNames: value ? fetchEnvironmentNames(bridge) : null,
75 + const setIsModalShowing: boolean => void = useCallback(
76 + (value: boolean) => {
77 + startTransition(() => {
78 + setContext({
79 + isModalShowing: value,
80 + setIsModalShowing,
81 + environmentNames: value ? fetchEnvironmentNames(bridge) : null,
82 + hookSettings: value ? fetchHookSettings(store) : null,
83 + });
84 });
63 - });
64 - });
85 + },
86 + [bridge, store],
87 + );
88
89 const [currentContext, setContext] = useState<Context>({
90 isModalShowing: false,
91 setIsModalShowing,
92 environmentNames: null,
93 + hookSettings: null,
94 });
95
96 return (
packages/react-devtools-shared/src/hook.js
+2
@@ -655,6 +655,8 @@ export function installHook(
655 Promise.resolve(maybeSettingsOrSettingsPromise)
656 .then(settings => {
657 hook.settings = settings;
658 + hook.emit('settingsInitialized', settings);
659 +
660 patchConsoleForErrorsAndWarnings();
661 })
662 .catch(() => {