mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
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:
committed by
Facebook GitHub Bot
parent
f8a181883f
commit
be255cbc16
-137
@@ -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 =
|
||||
|
||||
+7
-3
@@ -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;
|
||||
"
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user