refactor: DebuggingOverlayRegistry to handle trace updates (#41744)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/41744

Changelog: [Internal]

With these changes:
- DebuggingRegitry is responsible for listening to the events from React DevTools and
- AppContainer renders DebuggingOverlay component and subscribes with its reference to the DebuggingRegistry
- [Improvement] Since DebuggingRegistry is a singleton, it will only subscribe to the React DevTools events once and not *number-of-rendered-AppContainers* times

All required functionality for highlighting elements on a single AppContainer will be added in one of the next diffs of this stack, changes are incremental.

Reviewed By: sammy-SC

Differential Revision: D51603860

fbshipit-source-id: 92b029eb54ef63b27af970770eb522915578a0b9
This commit is contained in:
Ruslan Lesiutin
2024-01-15 14:11:00 -08:00
committed by Facebook GitHub Bot
parent f8a181883f
commit be255cbc16
6 changed files with 229 additions and 155 deletions
@@ -1,137 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {Overlay} from '../../Debugging/DebuggingOverlayNativeComponent';
import type {
InstanceFromReactDevTools,
ReactDevToolsAgent,
} from '../../Types/ReactDevToolsTypes';
import DebuggingOverlayNativeComponent, {
Commands,
} from '../../Debugging/DebuggingOverlayNativeComponent';
import UIManager from '../../ReactNative/UIManager';
import processColor from '../../StyleSheet/processColor';
import StyleSheet from '../../StyleSheet/StyleSheet';
import View from '../View/View';
import * as React from 'react';
const {useEffect, useRef, useState} = React;
const isNativeComponentReady =
UIManager.hasViewManagerConfig('DebuggingOverlay');
type Props = {
reactDevToolsAgent: ReactDevToolsAgent,
};
export default function TraceUpdateOverlay({
reactDevToolsAgent,
}: Props): React.Node {
const [overlayDisabled, setOverlayDisabled] = useState(false);
useEffect(() => {
const drawTraceUpdates = (
nodesToDraw: Array<{node: InstanceFromReactDevTools, color: string}> = [],
) => {
if (!isNativeComponentReady) {
return;
}
// If overlay is disabled before, now it's enabled.
setOverlayDisabled(false);
const newFramesToDraw: Array<Promise<Overlay>> = [];
nodesToDraw.forEach(({node, color}) => {
// `canonical.publicInstance` => Fabric
// TODO: remove this check when syncing the new version of the renderer from React to React Native.
// `canonical` => Legacy Fabric
// `node` => Legacy renderer
const component =
(node.canonical && node.canonical.publicInstance) ??
node.canonical ??
node;
if (!component || !component.measure) {
return;
}
const frameToDrawPromise = new Promise<Overlay>(resolve => {
// The if statement here is to make flow happy
if (component.measure) {
// TODO(T145522797): We should refactor this to use `getBoundingClientRect` when Paper is no longer supported.
component.measure((x, y, width, height, left, top) => {
resolve({
rect: {left, top, width, height},
color: processColor(color),
});
});
}
});
newFramesToDraw.push(frameToDrawPromise);
});
Promise.all(newFramesToDraw).then(
results => {
if (nativeComponentRef.current != null) {
Commands.draw(
nativeComponentRef.current,
JSON.stringify(
results.filter(
({rect, color}) => rect.width >= 0 && rect.height >= 0,
),
),
);
}
},
err => {
console.error(`Failed to measure updated traces. Error: ${err}`);
},
);
};
const disableTraceUpdates = () => {
// When trace updates are disabled from the backend, we won't receive draw events until it's enabled by the next draw. We can safely remove the overlay as it's not needed now.
setOverlayDisabled(true);
};
reactDevToolsAgent.addListener('drawTraceUpdates', drawTraceUpdates);
reactDevToolsAgent.addListener('disableTraceUpdates', drawTraceUpdates);
return () => {
reactDevToolsAgent.removeListener('drawTraceUpdates', drawTraceUpdates);
reactDevToolsAgent.removeListener(
'disableTraceUpdates',
disableTraceUpdates,
);
};
}, [reactDevToolsAgent]);
const nativeComponentRef =
useRef<?React.ElementRef<typeof DebuggingOverlayNativeComponent>>(null);
return (
!overlayDisabled &&
isNativeComponentReady && (
<View pointerEvents="none" style={styles.overlay}>
<DebuggingOverlayNativeComponent
ref={nativeComponentRef}
style={styles.overlay}
/>
</View>
)
);
}
const styles = StyleSheet.create({
overlay: {
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
},
});
@@ -0,0 +1,88 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {Overlay} from './DebuggingOverlayNativeComponent';
import View from '../Components/View/View';
import UIManager from '../ReactNative/UIManager';
import StyleSheet from '../StyleSheet/StyleSheet';
import DebuggingOverlayNativeComponent, {
Commands,
} from './DebuggingOverlayNativeComponent';
import * as React from 'react';
const {useRef, useImperativeHandle} = React;
const isNativeComponentReady =
UIManager.hasViewManagerConfig('DebuggingOverlay');
type DebuggingOverlayHandle = {
highlightTraceUpdates(updates: Overlay[]): void,
};
function DebuggingOverlay(
_props: {},
ref: React.RefSetter<DebuggingOverlayHandle>,
): React.Node {
useImperativeHandle(
ref,
() => ({
highlightTraceUpdates(updates) {
if (!isNativeComponentReady) {
return;
}
const nonEmptyRectangles = updates.filter(
({rect, color}) => rect.width >= 0 && rect.height >= 0,
);
if (nativeComponentRef.current != null) {
Commands.draw(
nativeComponentRef.current,
JSON.stringify(nonEmptyRectangles),
);
}
},
}),
[],
);
const nativeComponentRef = useRef<React.ElementRef<
typeof DebuggingOverlayNativeComponent,
> | null>(null);
return (
isNativeComponentReady && (
<View pointerEvents="none" style={styles.overlay}>
<DebuggingOverlayNativeComponent
ref={nativeComponentRef}
style={styles.overlay}
/>
</View>
)
);
}
const styles = StyleSheet.create({
overlay: {
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
},
});
const DebuggingOverlayWithForwardedRef: React.AbstractComponent<
{},
DebuggingOverlayHandle,
React.Node,
> = React.forwardRef(DebuggingOverlay);
export default DebuggingOverlayWithForwardedRef;
@@ -9,14 +9,46 @@
* @oncall react_native
*/
import type {AppContainerRootViewRef} from '../ReactNative/AppContainer-dev';
import type {
AppContainerRootViewRef,
DebuggingOverlayRef,
} from '../ReactNative/AppContainer-dev';
import type {NativeMethods} from '../Renderer/shims/ReactNativeTypes';
import type {
InstanceFromReactDevTools,
ReactDevToolsAgent,
ReactDevToolsAgentEvents,
ReactDevToolsGlobalHook,
} from '../Types/ReactDevToolsTypes';
import type {Overlay} from './DebuggingOverlayNativeComponent';
import processColor from '../StyleSheet/processColor';
// TODO(T171193075): __REACT_DEVTOOLS_GLOBAL_HOOK__ is always injected in dev-bundles,
// but it is not mocked in some Jest tests. We should update Jest tests setup, so it would be the same as expected testing environment.
const reactDevToolsHook: ?ReactDevToolsGlobalHook =
window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
export type DebuggingOverlayRegistrySubscriberProtocol = {
rootViewRef: AppContainerRootViewRef,
debuggingOverlayRef: DebuggingOverlayRef,
};
class DebuggingOverlayRegistry {
#registry: Set<DebuggingOverlayRegistrySubscriberProtocol> = new Set();
#reactDevToolsAgent: ReactDevToolsAgent | null = null;
constructor() {
if (reactDevToolsHook?.reactDevtoolsAgent != null) {
this.#onReactDevToolsAgentAttached(reactDevToolsHook.reactDevtoolsAgent);
return;
}
reactDevToolsHook?.on?.(
'react-devtools',
this.#onReactDevToolsAgentAttached,
);
}
subscribe(subscriber: DebuggingOverlayRegistrySubscriberProtocol) {
this.#registry.add(subscriber);
@@ -31,6 +63,74 @@ class DebuggingOverlayRegistry {
);
}
}
#onReactDevToolsAgentAttached = (agent: ReactDevToolsAgent): void => {
this.#reactDevToolsAgent = agent;
agent.addListener('drawTraceUpdates', this.#onDrawTraceUpdates);
};
#getPublicInstanceFromInstance(
instanceHandle: InstanceFromReactDevTools,
): NativeMethods | null {
// `canonical.publicInstance` => Fabric
if (instanceHandle.canonical?.publicInstance != null) {
return instanceHandle.canonical?.publicInstance;
}
// `canonical` => Legacy Fabric
if (instanceHandle.canonical != null) {
// $FlowFixMe[incompatible-return]
return instanceHandle.canonical;
}
// `instanceHandle` => Legacy renderer
if (instanceHandle.measure != null) {
// $FlowFixMe[incompatible-return]
return instanceHandle;
}
return null;
}
#onDrawTraceUpdates: (
...ReactDevToolsAgentEvents['drawTraceUpdates']
) => void = traceUpdates => {
const promisesToResolve: Array<Promise<Overlay>> = [];
traceUpdates.forEach(({node, color}) => {
const publicInstance = this.#getPublicInstanceFromInstance(node);
if (publicInstance == null) {
return;
}
const frameToDrawPromise = new Promise<Overlay>(resolve => {
// TODO(T171095283): We should refactor this to use `getBoundingClientRect` when Paper is no longer supported.
publicInstance.measure((x, y, width, height, left, top) => {
resolve({
rect: {left, top, width, height},
color: processColor(color),
});
});
});
promisesToResolve.push(frameToDrawPromise);
});
Promise.all(promisesToResolve).then(
updates => {
for (const subscriber of this.#registry) {
subscriber.debuggingOverlayRef.current?.highlightTraceUpdates(
updates,
);
}
},
err => {
console.error(`Failed to measure updated traces. Error: ${err}`);
},
);
};
}
const debuggingOverlayRegistryInstance: DebuggingOverlayRegistry =
@@ -9,20 +9,24 @@
* @oncall react_native
*/
import type {AppContainerRootViewRef} from '../ReactNative/AppContainer-dev';
import type {
AppContainerRootViewRef,
DebuggingOverlayRef,
} from '../ReactNative/AppContainer-dev';
import DebuggingOverlayRegistry from './DebuggingOverlayRegistry';
import {useEffect} from 'react';
const useSubscribeToDebuggingOverlayRegistry = (
rootViewRef: AppContainerRootViewRef,
debuggingOverlayRef: DebuggingOverlayRef,
) => {
useEffect(() => {
const subscriber = {rootViewRef};
const subscriber = {rootViewRef, debuggingOverlayRef};
DebuggingOverlayRegistry.subscribe(subscriber);
return () => DebuggingOverlayRegistry.unsubscribe(subscriber);
}, [rootViewRef]);
}, [rootViewRef, debuggingOverlayRef]);
};
export default useSubscribeToDebuggingOverlayRegistry;
@@ -15,9 +15,9 @@ import type {
} from '../Types/ReactDevToolsTypes';
import type {Props} from './AppContainer';
import TraceUpdateOverlay from '../Components/TraceUpdateOverlay/TraceUpdateOverlay';
import ReactNativeStyleAttributes from '../Components/View/ReactNativeStyleAttributes';
import View from '../Components/View/View';
import DebuggingOverlay from '../Debugging/DebuggingOverlay';
import useSubscribeToDebuggingOverlayRegistry from '../Debugging/useSubscribeToDebuggingOverlayRegistry';
import RCTDeviceEventEmitter from '../EventEmitter/RCTDeviceEventEmitter';
import ReactDevToolsOverlay from '../Inspector/ReactDevToolsOverlay';
@@ -76,7 +76,12 @@ const AppContainer = ({
}: Props): React.Node => {
const appContainerRootViewRef: AppContainerRootViewRef = React.useRef(null);
const innerViewRef: InspectedViewRef = React.useRef(null);
useSubscribeToDebuggingOverlayRegistry(appContainerRootViewRef);
const debuggingOverlayRef: DebuggingOverlayRef = React.useRef(null);
useSubscribeToDebuggingOverlayRegistry(
appContainerRootViewRef,
debuggingOverlayRef,
);
const [key, setKey] = useState(0);
const [shouldRenderInspector, setShouldRenderInspector] = useState(false);
@@ -147,9 +152,8 @@ const AppContainer = ({
pointerEvents="box-none">
{innerView}
{reactDevToolsAgent != null && (
<TraceUpdateOverlay reactDevToolsAgent={reactDevToolsAgent} />
)}
<DebuggingOverlay ref={debuggingOverlayRef} />
{reactDevToolsAgent != null && (
<ReactDevToolsOverlay
inspectedViewRef={innerViewRef}
@@ -181,4 +185,8 @@ export type AppContainerRootViewRef = React.RefObject<React.ElementRef<
export type InspectedViewRef = React.RefObject<React.ElementRef<
typeof View,
> | null>;
export type DebuggingOverlayRef = React.RefObject<React.ElementRef<
typeof DebuggingOverlay,
> | null>;
export default AppContainer;
@@ -3758,14 +3758,6 @@ declare module.exports: Touchable;
"
`;
exports[`public API should not change unintentionally Libraries/Components/TraceUpdateOverlay/TraceUpdateOverlay.js 1`] = `
"type Props = {
reactDevToolsAgent: ReactDevToolsAgent,
};
declare export default function TraceUpdateOverlay(Props): React.Node;
"
`;
exports[`public API should not change unintentionally Libraries/Components/UnimplementedViews/UnimplementedNativeViewNativeComponent.js 1`] = `
"type NativeProps = $ReadOnly<{|
...ViewProps,
@@ -4757,6 +4749,19 @@ declare export function createNodeList<T>(
"
`;
exports[`public API should not change unintentionally Libraries/Debugging/DebuggingOverlay.js 1`] = `
"type DebuggingOverlayHandle = {
highlightTraceUpdates(updates: Overlay[]): void,
};
declare const DebuggingOverlayWithForwardedRef: React.AbstractComponent<
{},
DebuggingOverlayHandle,
React.Node,
>;
declare export default typeof DebuggingOverlayWithForwardedRef;
"
`;
exports[`public API should not change unintentionally Libraries/Debugging/DebuggingOverlayNativeComponent.js 1`] = `
"type NativeProps = $ReadOnly<{|
...ViewProps,
@@ -4780,8 +4785,10 @@ declare export default HostComponent<NativeProps>;
exports[`public API should not change unintentionally Libraries/Debugging/DebuggingOverlayRegistry.js 1`] = `
"export type DebuggingOverlayRegistrySubscriberProtocol = {
rootViewRef: AppContainerRootViewRef,
debuggingOverlayRef: DebuggingOverlayRef,
};
declare class DebuggingOverlayRegistry {
constructor(): void;
subscribe(subscriber: DebuggingOverlayRegistrySubscriberProtocol): void;
unsubscribe(subscriber: DebuggingOverlayRegistrySubscriberProtocol): void;
}
@@ -4792,7 +4799,8 @@ declare export default typeof debuggingOverlayRegistryInstance;
exports[`public API should not change unintentionally Libraries/Debugging/useSubscribeToDebuggingOverlayRegistry.js 1`] = `
"declare const useSubscribeToDebuggingOverlayRegistry: (
rootViewRef: AppContainerRootViewRef
rootViewRef: AppContainerRootViewRef,
debuggingOverlayRef: DebuggingOverlayRef
) => void;
declare export default typeof useSubscribeToDebuggingOverlayRegistry;
"
@@ -7546,6 +7554,9 @@ export type AppContainerRootViewRef = React.RefObject<React.ElementRef<
export type InspectedViewRef = React.RefObject<React.ElementRef<
typeof View,
> | null>;
export type DebuggingOverlayRef = React.RefObject<React.ElementRef<
typeof DebuggingOverlay,
> | null>;
declare export default typeof AppContainer;
"
`;