diff --git a/Libraries/Renderer/src/renderers/native/ReactNativeFiber.js b/Libraries/Renderer/src/renderers/native/ReactNativeFiber.js index a43e51f2088c35..0b05e0d32a54fa 100644 --- a/Libraries/Renderer/src/renderers/native/ReactNativeFiber.js +++ b/Libraries/Renderer/src/renderers/native/ReactNativeFiber.js @@ -12,10 +12,12 @@ 'use strict'; +const ReactFiberErrorLogger = require('ReactFiberErrorLogger'); const ReactFiberReconciler = require('ReactFiberReconciler'); const ReactGenericBatching = require('ReactGenericBatching'); const ReactNativeAttributePayload = require('ReactNativeAttributePayload'); const ReactNativeComponentTree = require('ReactNativeComponentTree'); +const ReactNativeFiberErrorDialog = require('ReactNativeFiberErrorDialog'); const ReactNativeFiberHostComponent = require('ReactNativeFiberHostComponent'); const ReactNativeInjection = require('ReactNativeInjection'); const ReactNativeTagHandles = require('ReactNativeTagHandles'); @@ -377,6 +379,13 @@ findNodeHandle.injection.injectFindNode((fiber: Fiber) => NativeRenderer.findHostInstance(fiber)); findNodeHandle.injection.injectFindRootNodeID(instance => instance); + +// Intercept lifecycle errors and ensure they are shown with the correct stack +// trace within the native redbox component. +ReactFiberErrorLogger.injection.injectDialog( + ReactNativeFiberErrorDialog.showDialog, +); + const ReactNative = { // External users of findNodeHandle() expect the host tag number return type. // The injected findNodeHandle() strategy returns the instance wrapper though. diff --git a/Libraries/Renderer/src/renderers/native/ReactNativeFiberErrorDialog.js b/Libraries/Renderer/src/renderers/native/ReactNativeFiberErrorDialog.js new file mode 100644 index 00000000000000..3d65ac46630396 --- /dev/null +++ b/Libraries/Renderer/src/renderers/native/ReactNativeFiberErrorDialog.js @@ -0,0 +1,57 @@ +/** + * Copyright 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactNativeFiberErrorDialog + * @flow + */ + +'use strict'; + +const ExceptionsManager = require('ExceptionsManager'); + +import type {CapturedError} from 'ReactFiberScheduler'; + +/** + * Intercept lifecycle errors and ensure they are shown with the correct stack + * trace within the native redbox component. + */ +function ReactNativeFiberErrorDialog(capturedError: CapturedError): boolean { + const {componentStack, error} = capturedError; + + let errorMessage: string; + let errorStack: string; + let errorType: Class; + + // Typically Errors are thrown but eg strings or null can be thrown as well. + if (error && typeof error === 'object') { + const {message, name} = error; + + const summary = message ? `${name}: ${message}` : name; + + errorMessage = `${summary}\n\nThis error is located at:${componentStack}`; + errorStack = error.stack; + errorType = error.constructor; + } else { + errorMessage = `Unspecified error at:${componentStack}`; + errorStack = ''; + errorType = Error; + } + + const newError = new errorType(errorMessage); + newError.stack = errorStack; + + ExceptionsManager.handleException(newError, false); + + // Return false here to prevent ReactFiberErrorLogger default behavior of + // logging error details to console.error. Calls to console.error are + // automatically routed to the native redbox controller, which we've already + // done above by calling ExceptionsManager. + return false; +} + +module.exports.showDialog = ReactNativeFiberErrorDialog; diff --git a/Libraries/Renderer/src/renderers/shared/fiber/ReactFiberErrorLogger.js b/Libraries/Renderer/src/renderers/shared/fiber/ReactFiberErrorLogger.js index 4deb6549322a72..7eb1f746b1e608 100644 --- a/Libraries/Renderer/src/renderers/shared/fiber/ReactFiberErrorLogger.js +++ b/Libraries/Renderer/src/renderers/shared/fiber/ReactFiberErrorLogger.js @@ -12,14 +12,23 @@ 'use strict'; -const emptyFunction = require('fbjs/lib/emptyFunction'); const invariant = require('fbjs/lib/invariant'); import type {CapturedError} from 'ReactFiberScheduler'; -let showDialog = emptyFunction; +const defaultShowDialog = () => true; + +let showDialog = defaultShowDialog; function logCapturedError(capturedError: CapturedError): void { + const logError = showDialog(capturedError); + + // Allow injected showDialog() to prevent default console.error logging. + // This enables renderers like ReactNative to better manage redbox behavior. + if (logError === false) { + return; + } + if (__DEV__) { const { componentName, @@ -85,14 +94,16 @@ function logCapturedError(capturedError: CapturedError): void { `React caught an error thrown by one of your components.\n\n${error.stack}`, ); } - - showDialog(capturedError); } exports.injection = { - injectDialog(fn: (e: CapturedError) => void) { + /** + * Display custom dialogĀ for lifecycle errors. + * Return false to prevent default behavior of logging to console.error. + */ + injectDialog(fn: (e: CapturedError) => boolean) { invariant( - showDialog === emptyFunction, + showDialog === defaultShowDialog, 'The custom dialog was already injected.', ); invariant( diff --git a/Libraries/Renderer/src/renderers/shared/fiber/ReactFiberScheduler.js b/Libraries/Renderer/src/renderers/shared/fiber/ReactFiberScheduler.js index 29b74134ec3831..2ffc72f74b8eaa 100644 --- a/Libraries/Renderer/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/Libraries/Renderer/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -684,6 +684,9 @@ module.exports = function( return null; } } + + // Without this explicit null return Flow complains of invalid return type + return null; } function performUnitOfWork(workInProgress: Fiber): Fiber | null {