Skip to content

Commit

Permalink
LogBox - Update to use it's own root for the inspector
Browse files Browse the repository at this point in the history
Summary:
Apologies for the large diff, it was difficult to break down.

This diff is a major refactor of LogBox that:
- Separates LogBoxNotification and LogBoxInspector
- Moves them each to their own container
- Updates AppContainer to only render the notification
- Updates the native logbox root to render the inspector
- Adds withSubscription HOC to manage subscribing to LogBoxData
- Simplifies LogBox to export an object instead of a component

Changelog: [Internal]

Reviewed By: motiz88

Differential Revision: D18750011

fbshipit-source-id: 639885d29e55e125892d1c2b6bbf2826f27d78db
  • Loading branch information
rickhanlonii authored and facebook-github-bot committed Dec 10, 2019
1 parent e272089 commit 5879795
Show file tree
Hide file tree
Showing 19 changed files with 613 additions and 557 deletions.
95 changes: 95 additions & 0 deletions Libraries/LogBox/Data/LogBoxData.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

('use strict');

import * as React from 'react';
import LogBoxLog from './LogBoxLog';
import {parseLogBoxException} from './parseLogBoxLog';
import type {LogLevel} from './LogBoxLog';
Expand Down Expand Up @@ -369,3 +370,97 @@ export function observe(observer: Observer): Subscription {
},
};
}

type Props = $ReadOnly<{||}>;
type State = $ReadOnly<{|
logs: LogBoxLogs,
isDisabled: boolean,
hasError: boolean,
selectedLogIndex: number,
|}>;

type SubscribedComponent = React.AbstractComponent<
$ReadOnly<{|
logs: $ReadOnlyArray<LogBoxLog>,
isDisabled: boolean,
selectedLogIndex: number,
|}>,
>;

export function withSubscription(
WrappedComponent: SubscribedComponent,
): React.AbstractComponent<{||}> {
class LogBoxStateSubscription extends React.Component<Props, State> {
static getDerivedStateFromError() {
return {hasError: true};
}

componentDidCatch(err: Error, errorInfo: {componentStack: string, ...}) {
reportLogBoxError(err, errorInfo.componentStack);
}

_subscription: ?Subscription;

state = {
logs: new Set(),
isDisabled: false,
hasError: false,
selectedLogIndex: -1,
};

render(): React.Node {
if (this.state.hasError) {
// This happens when the component failed to render, in which case we delegate to the native redbox.
// We can't show anyback fallback UI here, because the error may be with <View> or <Text>.
return null;
}

return (
<WrappedComponent
logs={Array.from(this.state.logs)}
isDisabled={this.state.isDisabled}
selectedLogIndex={this.state.selectedLogIndex}
/>
);
}

componentDidMount(): void {
this._subscription = observe(data => {
this.setState(data);
});
}

componentWillUnmount(): void {
if (this._subscription != null) {
this._subscription.unsubscribe();
}
}

_handleDismiss = (): void => {
// Here we handle the cases when the log is dismissed and it
// was either the last log, or when the current index
// is now outside the bounds of the log array.
const {selectedLogIndex, logs: stateLogs} = this.state;
const logsArray = Array.from(stateLogs);
if (selectedLogIndex != null) {
if (logsArray.length - 1 <= 0) {
setSelectedLog(-1);
} else if (selectedLogIndex >= logsArray.length - 1) {
setSelectedLog(selectedLogIndex - 1);
}

dismiss(logsArray[selectedLogIndex]);
}
};

_handleMinimize = (): void => {
setSelectedLog(-1);
};

_handleSetSelectedLog = (index: number): void => {
setSelectedLog(index);
};
}

return LogBoxStateSubscription;
}
126 changes: 28 additions & 98 deletions Libraries/LogBox/LogBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,14 @@

'use strict';

import * as React from 'react';
import Platform from '../Utilities/Platform';
import RCTLog from '../Utilities/RCTLog';
import LogBoxContainer from './UI/LogBoxContainer';
import * as LogBoxData from './Data/LogBoxData';
import {parseLogBoxLog} from './Data/parseLogBoxLog';

import type {LogBoxLogs, Subscription, IgnorePattern} from './Data/LogBoxData';
import type {IgnorePattern} from './Data/LogBoxData';

import LogBoxLog from './Data/LogBoxLog';

type Props = $ReadOnly<{||}>;
type State = {|
logs: LogBoxLogs,
isDisabled: boolean,
hasError: boolean,
selectedLogIndex: number,
|};

let LogBoxComponent;
let LogBox;

/**
* LogBox displays logs in the app.
Expand All @@ -48,25 +36,26 @@ if (__DEV__) {
warnImpl(...args);
};

LogBoxComponent = class LogBox extends React.Component<Props, State> {
static getDerivedStateFromError() {
return {hasError: true};
}

componentDidCatch(err, errorInfo) {
LogBoxData.reportLogBoxError(err, errorInfo.componentStack);
}

LogBox = {
// TODO: deprecated, replace with ignoreLogs
static ignoreWarnings(patterns: $ReadOnlyArray<IgnorePattern>): void {
ignoreWarnings: (patterns: $ReadOnlyArray<IgnorePattern>): void => {
LogBox.ignoreLogs(patterns);
}
},

static ignoreLogs(patterns: $ReadOnlyArray<IgnorePattern>): void {
ignoreLogs: (patterns: $ReadOnlyArray<IgnorePattern>): void => {
LogBoxData.addIgnorePatterns(patterns);
}
},

uninstall: (): void => {
errorImpl = error;
warnImpl = warn;
delete (console: any).disableLogBox;
},

install: (): void => {
// Trigger lazy initialization of module.
require('../NativeModules/specs/NativeLogBox');

static install(): void {
errorImpl = function(...args) {
registerError(...args);
};
Expand All @@ -92,62 +81,7 @@ if (__DEV__) {
RCTLog.setWarningHandler((...args) => {
registerWarning(...args);
});
}

static uninstall(): void {
errorImpl = error;
warnImpl = warn;
delete (console: any).disableLogBox;
}

_subscription: ?Subscription;

state = {
logs: new Set(),
isDisabled: false,
hasError: false,
selectedLogIndex: -1,
};

render(): React.Node {
if (this.state.hasError) {
// This happens when the component failed to render, in which case we delegate to the native redbox.
// We can't show anyback fallback UI here, because the error may be with <View> or <Text>.
return null;
}

return this.state.logs == null ? null : (
<LogBoxContainer
onDismiss={this._handleDismiss}
onDismissWarns={LogBoxData.clearWarnings}
onDismissErrors={LogBoxData.clearErrors}
logs={this.state.logs}
isDisabled={this.state.isDisabled}
selectedLogIndex={this.state.selectedLogIndex}
setSelectedLog={this._handleSetSelectedLog}
/>
);
}

componentDidMount(): void {
this._subscription = LogBoxData.observe(data => {
this.setState(data);
});
}

componentWillUnmount(): void {
if (this._subscription != null) {
this._subscription.unsubscribe();
}
}

_handleDismiss(log: LogBoxLog): void {
LogBoxData.dismiss(log);
}

_handleSetSelectedLog(index: number): void {
LogBoxData.setSelectedLog(index);
}
},
};

const isRCTLogAdviceWarning = (...args) => {
Expand Down Expand Up @@ -227,31 +161,27 @@ if (__DEV__) {
}
};
} else {
LogBoxComponent = class extends React.Component<Props, State> {
LogBox = {
// TODO: deprecated, replace with ignoreLogs
static ignoreWarnings(patterns: $ReadOnlyArray<IgnorePattern>): void {
ignoreWarnings: (patterns: $ReadOnlyArray<IgnorePattern>): void => {
// Do nothing.
}
},

static ignoreLogs(patterns: $ReadOnlyArray<IgnorePattern>): void {
ignoreLogs: (patterns: $ReadOnlyArray<IgnorePattern>): void => {
// Do nothing.
}
},

static install(): void {
install: (): void => {
// Do nothing.
}
},

static uninstall(): void {
uninstall: (): void => {
// Do nothing.
}

render(): React.Node {
return null;
}
},
};
}

module.exports = (LogBoxComponent: Class<React.Component<Props, State>> & {
module.exports = (LogBox: {
// TODO: deprecated, replace with ignoreLogs
ignoreWarnings($ReadOnlyArray<IgnorePattern>): void,
ignoreLogs($ReadOnlyArray<IgnorePattern>): void,
Expand Down
86 changes: 86 additions & 0 deletions Libraries/LogBox/LogBoxInspectorContainer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/

'use strict';

import * as React from 'react';
import {View, StyleSheet} from 'react-native';
import * as LogBoxData from './Data/LogBoxData';
import LogBoxInspector from './UI/LogBoxInspector';
import type LogBoxLog from './Data/LogBoxLog';
import NativeLogBox from '../NativeModules/specs/NativeLogBox';

type Props = $ReadOnly<{|
logs: $ReadOnlyArray<LogBoxLog>,
selectedLogIndex: number,
isDisabled?: ?boolean,
|}>;

function NativeLogBoxVisibility(props) {
React.useLayoutEffect(() => {
if (NativeLogBox) {
if (props.visible) {
// Schedule this to try and prevent flashing the old state.
setTimeout(() => NativeLogBox.show(), 10);
} else {
NativeLogBox.hide();
}
}
}, [props.visible]);

return props.children;
}

export class _LogBoxInspectorContainer extends React.Component<Props> {
render(): React.Node {
return (
<NativeLogBoxVisibility visible={this.props.selectedLogIndex >= 0}>
<View style={StyleSheet.absoluteFill}>
<LogBoxInspector
onDismiss={this._handleDismiss}
onMinimize={this._handleMinimize}
onChangeSelectedIndex={this._handleSetSelectedLog}
logs={this.props.logs}
selectedIndex={this.props.selectedLogIndex}
/>
</View>
</NativeLogBoxVisibility>
);
}

_handleDismiss = (): void => {
// Here we handle the cases when the log is dismissed and it
// was either the last log, or when the current index
// is now outside the bounds of the log array.
const {selectedLogIndex, logs} = this.props;
const logsArray = Array.from(logs);
if (selectedLogIndex != null) {
if (logsArray.length - 1 <= 0) {
LogBoxData.setSelectedLog(-1);
} else if (selectedLogIndex >= logsArray.length - 1) {
LogBoxData.setSelectedLog(selectedLogIndex - 1);
}

LogBoxData.dismiss(logsArray[selectedLogIndex]);
}
};

_handleMinimize = (): void => {
LogBoxData.setSelectedLog(-1);
};

_handleSetSelectedLog = (index: number): void => {
LogBoxData.setSelectedLog(index);
};
}

export default (LogBoxData.withSubscription(
_LogBoxInspectorContainer,
): React.AbstractComponent<{||}>);
Loading

0 comments on commit 5879795

Please sign in to comment.