refactor[AppContainer]: migrate to functional component (#41285)

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

Changelog: [Internal]

I have an ongoing work on improving debugging overlays, lots of changes in these files are planned.

- Split `AppContainer` into 2 implementations: DEV and PROD
- Rewrite to functional components

Reviewed By: javache

Differential Revision: D50559549

fbshipit-source-id: 3790f149d504bd45be78c6f3103e05677f3a107d
This commit is contained in:
Ruslan Lesiutin
2023-11-09 11:09:10 -08:00
committed by Facebook GitHub Bot
parent 19945ad83b
commit 9cc46f58ad
4 changed files with 221 additions and 161 deletions
+8 -6
View File
@@ -29,13 +29,17 @@ const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
// Required for React DevTools to view/edit React Native styles in Flipper.
// Flipper doesn't inject these values when initializing DevTools.
hook.resolveRNStyle = require('../StyleSheet/flattenStyle');
hook.nativeStyleEditorValidAttributes = Object.keys(ReactNativeStyleAttributes);
if (hook) {
hook.resolveRNStyle = require('../StyleSheet/flattenStyle');
hook.nativeStyleEditorValidAttributes = Object.keys(
ReactNativeStyleAttributes,
);
}
class Inspector extends React.Component<
{
inspectedView: ?HostRef,
onRequestRerenderApp: (callback: (instance: ?HostRef) => void) => void,
onRequestRerenderApp: () => void,
...
},
{
@@ -194,9 +198,7 @@ class Inspector extends React.Component<
setTouchTargeting(val: boolean) {
PressabilityDebug.setEnabled(val);
this.props.onRequestRerenderApp(inspectedView => {
this.setState({inspectedView});
});
this.props.onRequestRerenderApp();
}
setNetworking(val: boolean) {
@@ -0,0 +1,154 @@
/**
* 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
* @oncall react_native
*/
import type {Props} from './AppContainer';
import TraceUpdateOverlay from '../Components/TraceUpdateOverlay/TraceUpdateOverlay';
import View from '../Components/View/View';
import ViewNativeComponent from '../Components/View/ViewNativeComponent';
import RCTDeviceEventEmitter from '../EventEmitter/RCTDeviceEventEmitter';
import ReactDevToolsOverlay from '../Inspector/DevtoolsOverlay';
import LogBoxNotificationContainer from '../LogBox/LogBoxNotificationContainer';
import StyleSheet from '../StyleSheet/StyleSheet';
import {RootTagContext, createRootTag} from './RootTag';
import * as React from 'react';
const {useEffect, useState, useCallback} = React;
const reactDevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
type InspectorDeferredProps = {
inspectedView: React.ElementRef<typeof ViewNativeComponent> | null,
onInspectedViewRerenderRequest: () => void,
};
const InspectorDeferred = ({
inspectedView,
onInspectedViewRerenderRequest,
}: InspectorDeferredProps) => {
// D39382967 adds a require cycle: InitializeCore -> AppContainer -> Inspector -> InspectorPanel -> ScrollView -> InitializeCore
// We can't remove it yet, fallback to dynamic require for now. This is the only reason why this logic is in a separate function.
const Inspector = require('../Inspector/Inspector');
return (
<Inspector
inspectedView={inspectedView}
onRequestRerenderApp={onInspectedViewRerenderRequest}
/>
);
};
const AppContainer = ({
children,
fabric,
initialProps,
internal_excludeInspector = false,
internal_excludeLogBox = false,
rootTag,
showArchitectureIndicator,
WrapperComponent,
}: Props): React.Node => {
const [mainRef, setMainRef] = useState<React.ElementRef<
typeof ViewNativeComponent,
> | null>(null);
const [key, setKey] = useState(0);
const [shouldRenderInspector, setShouldRenderInspector] = useState(false);
const [shouldRenderDebuggingOverlays, setShouldRenderDebuggingOverlays] =
useState(reactDevToolsHook?.reactDevtoolsAgent != null);
useEffect(() => {
let inspectorSubscription = null;
if (!internal_excludeInspector) {
inspectorSubscription = RCTDeviceEventEmitter.addListener(
'toggleElementInspector',
() => setShouldRenderInspector(value => !value),
);
}
let reactDevToolsAgentListener = null;
// Subscribe listener, if agent is not attached yet
if (
reactDevToolsHook != null &&
reactDevToolsHook.reactDevtoolsAgent == null
) {
reactDevToolsAgentListener = () => setShouldRenderDebuggingOverlays(true);
reactDevToolsHook.on?.('react-devtools', reactDevToolsAgentListener);
}
return () => {
inspectorSubscription?.remove();
if (
reactDevToolsHook?.off != null &&
reactDevToolsAgentListener != null
) {
reactDevToolsHook.off('react-devtools', reactDevToolsAgentListener);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
let innerView: React.Node = (
<View
collapsable={!shouldRenderDebuggingOverlays && !shouldRenderInspector}
pointerEvents="box-none"
key={key}
style={styles.container}
ref={setMainRef}>
{children}
</View>
);
if (WrapperComponent != null) {
innerView = (
<WrapperComponent
initialProps={initialProps}
fabric={fabric === true}
showArchitectureIndicator={showArchitectureIndicator === true}>
{innerView}
</WrapperComponent>
);
}
const onInspectedViewRerenderRequest = useCallback(
() => setKey(k => k + 1),
[],
);
return (
<RootTagContext.Provider value={createRootTag(rootTag)}>
<View style={styles.container} pointerEvents="box-none">
{innerView}
{shouldRenderDebuggingOverlays && <TraceUpdateOverlay />}
{shouldRenderDebuggingOverlays && (
<ReactDevToolsOverlay inspectedView={mainRef} />
)}
{shouldRenderInspector && (
<InspectorDeferred
inspectedView={mainRef}
onInspectedViewRerenderRequest={onInspectedViewRerenderRequest}
/>
)}
{!internal_excludeLogBox && <LogBoxNotificationContainer />}
</View>
</RootTagContext.Provider>
);
};
const styles = StyleSheet.create({
container: {flex: 1},
});
export default AppContainer;
@@ -0,0 +1,53 @@
/**
* 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
* @oncall react_native
*/
import type {Props} from './AppContainer';
import View from '../Components/View/View';
import StyleSheet from '../StyleSheet/StyleSheet';
import {RootTagContext, createRootTag} from './RootTag';
import * as React from 'react';
const AppContainer = ({
children,
fabric,
initialProps,
rootTag,
showArchitectureIndicator,
WrapperComponent,
}: Props): React.Node => {
let innerView = children;
if (WrapperComponent != null) {
innerView = (
<WrapperComponent
initialProps={initialProps}
fabric={fabric === true}
showArchitectureIndicator={showArchitectureIndicator === true}>
{innerView}
</WrapperComponent>
);
}
return (
<RootTagContext.Provider value={createRootTag(rootTag)}>
<View style={styles.root} pointerEvents="box-none">
{innerView}
</View>
</RootTagContext.Provider>
);
};
const styles = StyleSheet.create({
root: {flex: 1},
});
export default AppContainer;
+6 -155
View File
@@ -10,170 +10,21 @@
import type {RootTag} from './RootTag';
import View from '../Components/View/View';
import RCTDeviceEventEmitter from '../EventEmitter/RCTDeviceEventEmitter';
import StyleSheet from '../StyleSheet/StyleSheet';
import {type EventSubscription} from '../vendor/emitter/EventEmitter';
import {RootTagContext, createRootTag} from './RootTag';
import * as React from 'react';
const reactDevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
type Props = $ReadOnly<{|
export type Props = $ReadOnly<{|
children?: React.Node,
fabric?: boolean,
rootTag: number | RootTag,
initialProps?: {...},
showArchitectureIndicator?: boolean,
WrapperComponent?: ?React.ComponentType<any>,
internal_excludeLogBox?: ?boolean,
internal_excludeInspector?: ?boolean,
internal_excludeLogBox?: boolean,
internal_excludeInspector?: boolean,
|}>;
type State = {|
inspector: ?React.Node,
devtoolsOverlay: ?React.Node,
traceUpdateOverlay: ?React.Node,
mainKey: number,
|};
class AppContainer extends React.Component<Props, State> {
state: State = {
inspector: null,
devtoolsOverlay: null,
traceUpdateOverlay: null,
mainKey: 1,
};
_mainRef: ?React.ElementRef<typeof View>;
_subscription: ?EventSubscription = null;
_reactDevToolsAgentListener: ?() => void = null;
static getDerivedStateFromError: any = undefined;
mountReactDevToolsOverlays(): void {
if (__DEV__) {
const DevtoolsOverlay = require('../Inspector/DevtoolsOverlay').default;
const devtoolsOverlay = <DevtoolsOverlay inspectedView={this._mainRef} />;
const TraceUpdateOverlay =
require('../Components/TraceUpdateOverlay/TraceUpdateOverlay').default;
const traceUpdateOverlay = <TraceUpdateOverlay />;
this.setState({devtoolsOverlay, traceUpdateOverlay});
}
}
componentDidMount(): void {
if (__DEV__) {
if (!this.props.internal_excludeInspector) {
this._subscription = RCTDeviceEventEmitter.addListener(
'toggleElementInspector',
() => {
const Inspector = require('../Inspector/Inspector');
const inspector = this.state.inspector ? null : (
<Inspector
inspectedView={this._mainRef}
onRequestRerenderApp={updateInspectedView => {
this.setState(
s => ({mainKey: s.mainKey + 1}),
() => updateInspectedView(this._mainRef),
);
}}
/>
);
this.setState({inspector});
},
);
if (reactDevToolsHook != null) {
if (reactDevToolsHook.reactDevtoolsAgent) {
// In case if this is not the first AppContainer rendered and React DevTools are already attached
this.mountReactDevToolsOverlays();
return;
}
this._reactDevToolsAgentListener = () =>
this.mountReactDevToolsOverlays();
if (reactDevToolsHook.on != null) {
reactDevToolsHook.on(
'react-devtools',
this._reactDevToolsAgentListener,
);
}
}
}
}
}
componentWillUnmount(): void {
if (this._subscription != null) {
this._subscription.remove();
}
if (
reactDevToolsHook?.off != null &&
this._reactDevToolsAgentListener != null
) {
reactDevToolsHook.off('react-devtools', this._reactDevToolsAgentListener);
}
}
render(): React.Node {
let logBox = null;
if (__DEV__) {
if (!this.props.internal_excludeLogBox) {
const LogBoxNotificationContainer =
require('../LogBox/LogBoxNotificationContainer').default;
logBox = <LogBoxNotificationContainer />;
}
}
let innerView: React.Node = (
<View
collapsable={!this.state.inspector && !this.state.devtoolsOverlay}
key={this.state.mainKey}
pointerEvents="box-none"
style={styles.appContainer}
ref={ref => {
this._mainRef = ref;
}}>
{this.props.children}
</View>
);
const Wrapper = this.props.WrapperComponent;
if (Wrapper != null) {
innerView = (
<Wrapper
initialProps={this.props.initialProps}
fabric={this.props.fabric === true}
showArchitectureIndicator={
this.props.showArchitectureIndicator === true
}>
{innerView}
</Wrapper>
);
}
return (
<RootTagContext.Provider value={createRootTag(this.props.rootTag)}>
<View style={styles.appContainer} pointerEvents="box-none">
{innerView}
{this.state.traceUpdateOverlay}
{this.state.devtoolsOverlay}
{this.state.inspector}
{logBox}
</View>
</RootTagContext.Provider>
);
}
}
const styles = StyleSheet.create({
appContainer: {
flex: 1,
},
});
const AppContainer: React.AbstractComponent<Props> = __DEV__
? require('./AppContainer-dev').default
: require('./AppContainer-prod').default;
module.exports = AppContainer;