diff --git a/packages/react-native-renderer/src/ReactNativeRenderer.js b/packages/react-native-renderer/src/ReactNativeRenderer.js index 7a1f500a7aec6..a4fa8f707df0c 100644 --- a/packages/react-native-renderer/src/ReactNativeRenderer.js +++ b/packages/react-native-renderer/src/ReactNativeRenderer.js @@ -19,6 +19,8 @@ import ReactVersion from 'shared/ReactVersion'; // Module provided by RN: import UIManager from 'UIManager'; +import {getStackAddendumByWorkInProgressFiber} from 'shared/ReactFiberComponentTreeHook'; + import NativeMethodsMixin from './NativeMethodsMixin'; import ReactNativeBridgeEventPlugin from './ReactNativeBridgeEventPlugin'; import ReactNativeComponent from './ReactNativeComponent'; @@ -35,6 +37,14 @@ injectFindHostInstance(ReactNativeFiberRenderer.findHostInstance); ReactGenericBatching.injection.injectRenderer(ReactNativeFiberRenderer); +function computeComponentStackForErrorReporting(reactTag: number): string { + let fiber = ReactNativeComponentTree.getClosestInstanceFromNode(reactTag); + if (!fiber) { + return ''; + } + return getStackAddendumByWorkInProgressFiber(fiber); +} + const roots = new Map(); const ReactNativeRenderer: ReactNativeType = { @@ -99,6 +109,7 @@ const ReactNativeRenderer: ReactNativeType = { TouchHistoryMath, // PanResponder createReactNativeComponentClass, // RCTText, RCTView, ReactNativeART takeSnapshot, // react-native-implementation + computeComponentStackForErrorReporting, }, }; diff --git a/packages/react-native-renderer/src/__tests__/ReactNativeError-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactNativeError-test.internal.js new file mode 100644 index 0000000000000..4724db55f4621 --- /dev/null +++ b/packages/react-native-renderer/src/__tests__/ReactNativeError-test.internal.js @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + * @jest-environment node + */ + +'use strict'; + +let React; +let ReactNative; +let createReactNativeComponentClass; +let computeComponentStackForErrorReporting; + +function normalizeCodeLocInfo(str) { + return str && str.replace(/\(at .+?:\d+\)/g, '(at **)'); +} + +describe('ReactNativeError', () => { + beforeEach(() => { + jest.resetModules(); + + React = require('react'); + ReactNative = require('react-native-renderer'); + createReactNativeComponentClass = require('../createReactNativeComponentClass') + .default; + computeComponentStackForErrorReporting = + ReactNative.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED + .computeComponentStackForErrorReporting; + }); + + it('should be able to extract a component stack from a native view', () => { + const View = createReactNativeComponentClass('View', () => ({ + validAttributes: {foo: true}, + uiViewClassName: 'View', + })); + + const ref = React.createRef(); + + function FunctionalComponent(props) { + return props.children; + } + + class ClassComponent extends React.Component { + render() { + return ( + + + + ); + } + } + + ReactNative.render(, 1); + + let reactTag = ReactNative.findNodeHandle(ref.current); + + let componentStack = normalizeCodeLocInfo( + computeComponentStackForErrorReporting(reactTag), + ); + + if (__DEV__) { + expect(componentStack).toBe( + '\n' + + ' in View (at **)\n' + + ' in FunctionalComponent (at **)\n' + + ' in ClassComponent (at **)', + ); + } else { + expect(componentStack).toBe( + '\n' + + ' in View\n' + + ' in FunctionalComponent\n' + + ' in ClassComponent', + ); + } + }); +});