diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 082f94261bfd7..c0cec0db366cf 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -484,6 +484,7 @@ function createElement( type: mixed, key: mixed, props: mixed, + owner: null | ReactComponentInfo, // DEV-only ): React$Element { let element: any; if (__DEV__ && enableRefAsProp) { @@ -493,7 +494,7 @@ function createElement( type, key, props, - _owner: null, + _owner: owner, }: any); Object.defineProperty(element, 'ref', { enumerable: false, @@ -520,7 +521,7 @@ function createElement( props, // Record the component responsible for creating this element. - _owner: null, + _owner: owner, }: any); } @@ -854,7 +855,12 @@ function parseModelTuple( if (tuple[0] === REACT_ELEMENT_TYPE) { // TODO: Consider having React just directly accept these arrays as elements. // Or even change the ReactElement type to be an array. - return createElement(tuple[1], tuple[2], tuple[3]); + return createElement( + tuple[1], + tuple[2], + tuple[3], + __DEV__ ? (tuple: any)[4] : null, + ); } return value; } @@ -1132,12 +1138,14 @@ function resolveConsoleEntry( ); } - const payload: [string, string, string, mixed] = parseModel(response, value); + const payload: [string, string, null | ReactComponentInfo, string, mixed] = + parseModel(response, value); const methodName = payload[0]; // TODO: Restore the fake stack before logging. // const stackTrace = payload[1]; - const env = payload[2]; - const args = payload.slice(3); + // const owner = payload[2]; + const env = payload[3]; + const args = payload.slice(4); printToConsole(methodName, args, env); } @@ -1286,7 +1294,10 @@ function processFullRow( } case 68 /* "D" */: { if (__DEV__) { - const debugInfo = JSON.parse(row); + const debugInfo: ReactComponentInfo | ReactAsyncInfo = parseModel( + response, + row, + ); resolveDebugInfo(response, id, debugInfo); return; } diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 55956ddc93ecd..6fbd2360c82ef 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -214,7 +214,7 @@ describe('ReactFlight', () => { const rootModel = await ReactNoopFlightClient.read(transport); const greeting = rootModel.greeting; expect(greeting._debugInfo).toEqual( - __DEV__ ? [{name: 'Greeting', env: 'Server'}] : undefined, + __DEV__ ? [{name: 'Greeting', env: 'Server', owner: null}] : undefined, ); ReactNoop.render(greeting); }); @@ -241,7 +241,7 @@ describe('ReactFlight', () => { await act(async () => { const promise = ReactNoopFlightClient.read(transport); expect(promise._debugInfo).toEqual( - __DEV__ ? [{name: 'Greeting', env: 'Server'}] : undefined, + __DEV__ ? [{name: 'Greeting', env: 'Server', owner: null}] : undefined, ); ReactNoop.render(await promise); }); @@ -2072,19 +2072,21 @@ describe('ReactFlight', () => { await act(async () => { const promise = ReactNoopFlightClient.read(transport); expect(promise._debugInfo).toEqual( - __DEV__ ? [{name: 'ServerComponent', env: 'Server'}] : undefined, + __DEV__ + ? [{name: 'ServerComponent', env: 'Server', owner: null}] + : undefined, ); const result = await promise; const thirdPartyChildren = await result.props.children[1]; // We expect the debug info to be transferred from the inner stream to the outer. expect(thirdPartyChildren[0]._debugInfo).toEqual( __DEV__ - ? [{name: 'ThirdPartyComponent', env: 'third-party'}] + ? [{name: 'ThirdPartyComponent', env: 'third-party', owner: null}] : undefined, ); expect(thirdPartyChildren[1]._debugInfo).toEqual( __DEV__ - ? [{name: 'ThirdPartyLazyComponent', env: 'third-party'}] + ? [{name: 'ThirdPartyLazyComponent', env: 'third-party', owner: null}] : undefined, ); ReactNoop.render(result); @@ -2145,4 +2147,50 @@ describe('ReactFlight', () => { expect(loggedFn).not.toBe(foo); expect(loggedFn.toString()).toBe(foo.toString()); }); + + it('uses the server component debug info as the element owner in DEV', async () => { + function Container({children}) { + return children; + } + + function Greeting({firstName}) { + // We can't use JSX here because it'll use the Client React. + return ReactServer.createElement( + Container, + null, + ReactServer.createElement('span', null, 'Hello, ', firstName), + ); + } + + const model = { + greeting: ReactServer.createElement(Greeting, {firstName: 'Seb'}), + }; + + const transport = ReactNoopFlightServer.render(model); + + await act(async () => { + const rootModel = await ReactNoopFlightClient.read(transport); + const greeting = rootModel.greeting; + // We've rendered down to the span. + expect(greeting.type).toBe('span'); + if (__DEV__) { + const greetInfo = {name: 'Greeting', env: 'Server', owner: null}; + expect(greeting._debugInfo).toEqual([ + greetInfo, + {name: 'Container', env: 'Server', owner: greetInfo}, + ]); + // The owner that created the span was the outer server component. + // We expect the debug info to be referentially equal to the owner. + expect(greeting._owner).toBe(greeting._debugInfo[0]); + } else { + expect(greeting._debugInfo).toBe(undefined); + expect(greeting._owner).toBe( + gate(flags => flags.disableStringRefs) ? undefined : null, + ); + } + ReactNoop.render(greeting); + }); + + expect(ReactNoop).toMatchRenderedOutput(Hello, Seb); + }); }); diff --git a/packages/react-devtools-shared/src/__tests__/componentStacks-test.js b/packages/react-devtools-shared/src/__tests__/componentStacks-test.js index 3d17546e39d19..02e2ad6802f61 100644 --- a/packages/react-devtools-shared/src/__tests__/componentStacks-test.js +++ b/packages/react-devtools-shared/src/__tests__/componentStacks-test.js @@ -101,6 +101,7 @@ describe('component stack', () => { { name: 'ServerComponent', env: 'Server', + owner: null, }, ]; const Parent = () => ChildPromise; diff --git a/packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js b/packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js index 0e412a43f74c3..316245492c64f 100644 --- a/packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js +++ b/packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js @@ -33,10 +33,7 @@ import { import {disableLogs, reenableLogs} from './DevToolsConsolePatching'; let prefix; -export function describeBuiltInComponentFrame( - name: string, - ownerFn: void | null | Function, -): string { +export function describeBuiltInComponentFrame(name: string): string { if (prefix === undefined) { // Extract the VM specific prefix used by each line. try { @@ -51,10 +48,7 @@ export function describeBuiltInComponentFrame( } export function describeDebugInfoFrame(name: string, env: ?string): string { - return describeBuiltInComponentFrame( - name + (env ? ' (' + env + ')' : ''), - null, - ); + return describeBuiltInComponentFrame(name + (env ? ' (' + env + ')' : '')); } let reentry = false; @@ -292,7 +286,6 @@ export function describeNativeComponentFrame( export function describeClassComponentFrame( ctor: Function, - ownerFn: void | null | Function, currentDispatcherRef: CurrentDispatcherRef, ): string { return describeNativeComponentFrame(ctor, true, currentDispatcherRef); @@ -300,7 +293,6 @@ export function describeClassComponentFrame( export function describeFunctionComponentFrame( fn: Function, - ownerFn: void | null | Function, currentDispatcherRef: CurrentDispatcherRef, ): string { return describeNativeComponentFrame(fn, false, currentDispatcherRef); @@ -313,7 +305,6 @@ function shouldConstruct(Component: Function) { export function describeUnknownElementTypeFrameInDEV( type: any, - ownerFn: void | null | Function, currentDispatcherRef: CurrentDispatcherRef, ): string { if (!__DEV__) { @@ -330,15 +321,15 @@ export function describeUnknownElementTypeFrameInDEV( ); } if (typeof type === 'string') { - return describeBuiltInComponentFrame(type, ownerFn); + return describeBuiltInComponentFrame(type); } switch (type) { case SUSPENSE_NUMBER: case SUSPENSE_SYMBOL_STRING: - return describeBuiltInComponentFrame('Suspense', ownerFn); + return describeBuiltInComponentFrame('Suspense'); case SUSPENSE_LIST_NUMBER: case SUSPENSE_LIST_SYMBOL_STRING: - return describeBuiltInComponentFrame('SuspenseList', ownerFn); + return describeBuiltInComponentFrame('SuspenseList'); } if (typeof type === 'object') { switch (type.$$typeof) { @@ -346,7 +337,6 @@ export function describeUnknownElementTypeFrameInDEV( case FORWARD_REF_SYMBOL_STRING: return describeFunctionComponentFrame( type.render, - ownerFn, currentDispatcherRef, ); case MEMO_NUMBER: @@ -354,7 +344,6 @@ export function describeUnknownElementTypeFrameInDEV( // Memo may contain any component type so we recursively resolve it. return describeUnknownElementTypeFrameInDEV( type.type, - ownerFn, currentDispatcherRef, ); case LAZY_NUMBER: @@ -366,7 +355,6 @@ export function describeUnknownElementTypeFrameInDEV( // Lazy may contain any component type so we recursively resolve it. return describeUnknownElementTypeFrameInDEV( init(payload), - ownerFn, currentDispatcherRef, ); } catch (x) {} diff --git a/packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js b/packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js index 77f0adf37ea4e..6d99cf4f21e52 100644 --- a/packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js +++ b/packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js @@ -39,38 +39,30 @@ export function describeFiber( ClassComponent, } = workTagMap; - const owner: null | Function = __DEV__ - ? workInProgress._debugOwner - ? workInProgress._debugOwner.type - : null - : null; switch (workInProgress.tag) { case HostComponent: - return describeBuiltInComponentFrame(workInProgress.type, owner); + return describeBuiltInComponentFrame(workInProgress.type); case LazyComponent: - return describeBuiltInComponentFrame('Lazy', owner); + return describeBuiltInComponentFrame('Lazy'); case SuspenseComponent: - return describeBuiltInComponentFrame('Suspense', owner); + return describeBuiltInComponentFrame('Suspense'); case SuspenseListComponent: - return describeBuiltInComponentFrame('SuspenseList', owner); + return describeBuiltInComponentFrame('SuspenseList'); case FunctionComponent: case IndeterminateComponent: case SimpleMemoComponent: return describeFunctionComponentFrame( workInProgress.type, - owner, currentDispatcherRef, ); case ForwardRef: return describeFunctionComponentFrame( workInProgress.type.render, - owner, currentDispatcherRef, ); case ClassComponent: return describeClassComponentFrame( workInProgress.type, - owner, currentDispatcherRef, ); default: diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index 803ef4b76b549..1f333d1271d67 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -1952,15 +1952,24 @@ export function attach( const {key} = fiber; const displayName = getDisplayNameForFiber(fiber); const elementType = getElementTypeForFiber(fiber); - const {_debugOwner} = fiber; + const debugOwner = fiber._debugOwner; // Ideally we should call getFiberIDThrows() for _debugOwner, // since owners are almost always higher in the tree (and so have already been processed), // but in some (rare) instances reported in open source, a descendant mounts before an owner. // Since this is a DEV only field it's probably okay to also just lazily generate and ID here if needed. // See https://github.com/facebook/react/issues/21445 - const ownerID = - _debugOwner != null ? getOrGenerateFiberID(_debugOwner) : 0; + let ownerID: number; + if (debugOwner != null) { + if (typeof debugOwner.tag === 'number') { + ownerID = getOrGenerateFiberID((debugOwner: any)); + } else { + // TODO: Track Server Component Owners. + ownerID = 0; + } + } else { + ownerID = 0; + } const parentID = parentFiber ? getFiberIDThrows(parentFiber) : 0; const displayNameStringID = getStringID(displayName); @@ -3104,15 +3113,17 @@ export function attach( return null; } - const {_debugOwner} = fiber; - const owners: Array = [fiberToSerializedElement(fiber)]; - if (_debugOwner) { - let owner: null | Fiber = _debugOwner; - while (owner !== null) { - owners.unshift(fiberToSerializedElement(owner)); - owner = owner._debugOwner || null; + let owner = fiber._debugOwner; + while (owner != null) { + if (typeof owner.tag === 'number') { + const ownerFiber: Fiber = (owner: any); // Refined + owners.unshift(fiberToSerializedElement(ownerFiber)); + owner = ownerFiber._debugOwner; + } else { + // TODO: Track Server Component Owners. + break; } } @@ -3173,7 +3184,7 @@ export function attach( } const { - _debugOwner, + _debugOwner: debugOwner, stateNode, key, memoizedProps, @@ -3300,13 +3311,19 @@ export function attach( context = {value: context}; } - let owners = null; - if (_debugOwner) { - owners = ([]: Array); - let owner: null | Fiber = _debugOwner; - while (owner !== null) { - owners.push(fiberToSerializedElement(owner)); - owner = owner._debugOwner || null; + let owners: null | Array = null; + let owner = debugOwner; + while (owner != null) { + if (typeof owner.tag === 'number') { + const ownerFiber: Fiber = (owner: any); // Refined + if (owners === null) { + owners = []; + } + owners.push(fiberToSerializedElement(ownerFiber)); + owner = ownerFiber._debugOwner; + } else { + // TODO: Track Server Component Owners. + break; } } diff --git a/packages/react-native-renderer/src/ReactNativeFiberInspector.js b/packages/react-native-renderer/src/ReactNativeFiberInspector.js index f30012b2cf917..14d87f7a5502b 100644 --- a/packages/react-native-renderer/src/ReactNativeFiberInspector.js +++ b/packages/react-native-renderer/src/ReactNativeFiberInspector.js @@ -103,13 +103,21 @@ function getInspectorDataForInstance( } const fiber = findCurrentFiberUsingSlowPath(closestInstance); + if (fiber === null) { + // Might not be currently mounted. + return { + hierarchy: [], + props: emptyObject, + selectedIndex: null, + componentStack: '', + }; + } const fiberHierarchy = getOwnerHierarchy(fiber); const instance = lastNonHostInstance(fiberHierarchy); const hierarchy = createHierarchy(fiberHierarchy); const props = getHostProps(instance); const selectedIndex = fiberHierarchy.indexOf(instance); - const componentStack = - fiber !== null ? getStackByFiberInDevAndProd(fiber) : ''; + const componentStack = getStackByFiberInDevAndProd(fiber); return { closestInstance: instance, @@ -125,7 +133,7 @@ function getInspectorDataForInstance( ); } -function getOwnerHierarchy(instance: any) { +function getOwnerHierarchy(instance: Fiber) { const hierarchy: Array<$FlowFixMe> = []; traverseOwnerTreeUp(hierarchy, instance); return hierarchy; @@ -143,15 +151,17 @@ function lastNonHostInstance(hierarchy) { return hierarchy[0]; } -// $FlowFixMe[missing-local-annot] function traverseOwnerTreeUp( hierarchy: Array<$FlowFixMe>, - instance: any, + instance: Fiber, ): void { if (__DEV__ || enableGetInspectorDataForInstanceInProduction) { - if (instance) { - hierarchy.unshift(instance); - traverseOwnerTreeUp(hierarchy, instance._debugOwner); + hierarchy.unshift(instance); + const owner = instance._debugOwner; + if (owner != null && typeof owner.tag === 'number') { + traverseOwnerTreeUp(hierarchy, (owner: any)); + } else { + // TODO: Traverse Server Components owners. } } } diff --git a/packages/react-reconciler/src/ReactCurrentFiber.js b/packages/react-reconciler/src/ReactCurrentFiber.js index 6c283ed0d35f4..c52d55c0cce73 100644 --- a/packages/react-reconciler/src/ReactCurrentFiber.js +++ b/packages/react-reconciler/src/ReactCurrentFiber.js @@ -11,7 +11,7 @@ import type {Fiber} from './ReactInternalTypes'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import {getStackByFiberInDevAndProd} from './ReactFiberComponentStack'; -import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; +import {getComponentNameFromOwner} from 'react-reconciler/src/getComponentNameFromFiber'; const ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame; @@ -24,8 +24,8 @@ export function getCurrentFiberOwnerNameInDevOrNull(): string | null { return null; } const owner = current._debugOwner; - if (owner !== null && typeof owner !== 'undefined') { - return getComponentNameFromFiber(owner); + if (owner != null) { + return getComponentNameFromOwner(owner); } } return null; diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 7b1a6514ebd5f..1de7d42cfe7d0 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -68,7 +68,7 @@ import { TracingMarkerComponent, } from './ReactWorkTags'; import {OffscreenVisible} from './ReactFiberActivityComponent'; -import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; +import {getComponentNameFromOwner} from 'react-reconciler/src/getComponentNameFromFiber'; import {isDevToolsPresent} from './ReactFiberDevToolsHook'; import { resolveClassForHotReloading, @@ -110,6 +110,7 @@ import { attachOffscreenInstance, } from './ReactFiberCommitWork'; import {getHostContext} from './ReactFiberHostContext'; +import type {ReactComponentInfo} from '../../shared/ReactTypes'; export type {Fiber}; @@ -475,7 +476,7 @@ export function createFiberFromTypeAndProps( type: any, // React$ElementType key: null | string, pendingProps: any, - owner: null | Fiber, + owner: null | ReactComponentInfo | Fiber, mode: TypeOfMode, lanes: Lanes, ): Fiber { @@ -610,7 +611,7 @@ export function createFiberFromTypeAndProps( "it's defined in, or you might have mixed up default and " + 'named imports.'; } - const ownerName = owner ? getComponentNameFromFiber(owner) : null; + const ownerName = owner ? getComponentNameFromOwner(owner) : null; if (ownerName) { info += '\n\nCheck the render method of `' + ownerName + '`.'; } diff --git a/packages/react-reconciler/src/ReactFiberComponentStack.js b/packages/react-reconciler/src/ReactFiberComponentStack.js index f292cb51d10b4..193c27aef1e1a 100644 --- a/packages/react-reconciler/src/ReactFiberComponentStack.js +++ b/packages/react-reconciler/src/ReactFiberComponentStack.js @@ -29,29 +29,24 @@ import { } from 'shared/ReactComponentStackFrame'; function describeFiber(fiber: Fiber): string { - const owner: null | Function = __DEV__ - ? fiber._debugOwner - ? fiber._debugOwner.type - : null - : null; switch (fiber.tag) { case HostHoistable: case HostSingleton: case HostComponent: - return describeBuiltInComponentFrame(fiber.type, owner); + return describeBuiltInComponentFrame(fiber.type); case LazyComponent: - return describeBuiltInComponentFrame('Lazy', owner); + return describeBuiltInComponentFrame('Lazy'); case SuspenseComponent: - return describeBuiltInComponentFrame('Suspense', owner); + return describeBuiltInComponentFrame('Suspense'); case SuspenseListComponent: - return describeBuiltInComponentFrame('SuspenseList', owner); + return describeBuiltInComponentFrame('SuspenseList'); case FunctionComponent: case SimpleMemoComponent: - return describeFunctionComponentFrame(fiber.type, owner); + return describeFunctionComponentFrame(fiber.type); case ForwardRef: - return describeFunctionComponentFrame(fiber.type.render, owner); + return describeFunctionComponentFrame(fiber.type.render); case ClassComponent: - return describeClassComponentFrame(fiber.type, owner); + return describeClassComponentFrame(fiber.type); default: return ''; } diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index 3b82dc40c828b..f12b9a16c570b 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -15,6 +15,7 @@ import type { Usable, ReactFormState, Awaited, + ReactComponentInfo, ReactDebugInfo, } from 'shared/ReactTypes'; import type {WorkTag} from './ReactWorkTags'; @@ -193,7 +194,7 @@ export type Fiber = { // __DEV__ only _debugInfo?: ReactDebugInfo | null, - _debugOwner?: Fiber | null, + _debugOwner?: ReactComponentInfo | Fiber | null, _debugIsCurrentlyTiming?: boolean, _debugNeedsRemount?: boolean, diff --git a/packages/react-reconciler/src/getComponentNameFromFiber.js b/packages/react-reconciler/src/getComponentNameFromFiber.js index 6659b14b7f0d0..332324900fa88 100644 --- a/packages/react-reconciler/src/getComponentNameFromFiber.js +++ b/packages/react-reconciler/src/getComponentNameFromFiber.js @@ -47,6 +47,7 @@ import { } from 'react-reconciler/src/ReactWorkTags'; import getComponentNameFromType from 'shared/getComponentNameFromType'; import {REACT_STRICT_MODE_TYPE} from 'shared/ReactSymbols'; +import type {ReactComponentInfo} from '../../shared/ReactTypes'; // Keep in sync with shared/getComponentNameFromType function getWrappedName( @@ -66,6 +67,18 @@ function getContextName(type: ReactContext) { return type.displayName || 'Context'; } +export function getComponentNameFromOwner( + owner: Fiber | ReactComponentInfo, +): string | null { + if (typeof owner.tag === 'number') { + return getComponentNameFromFiber((owner: any)); + } + if (typeof owner.name === 'string') { + return owner.name; + } + return null; +} + export default function getComponentNameFromFiber(fiber: Fiber): string | null { const {tag, type} = fiber; switch (tag) { diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js index cce874d8148db..8304d9927d372 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js @@ -290,7 +290,7 @@ describe('ReactFlightDOMEdge', () => { , ); const serializedContent = await readResult(stream); - const expectedDebugInfoSize = __DEV__ ? 42 * 20 : 0; + const expectedDebugInfoSize = __DEV__ ? 64 * 20 : 0; expect(serializedContent.length).toBeLessThan(150 + expectedDebugInfoSize); }); diff --git a/packages/react-server/src/ReactFizzComponentStack.js b/packages/react-server/src/ReactFizzComponentStack.js index 7aeb6b0802c92..84b4d82d8d45f 100644 --- a/packages/react-server/src/ReactFizzComponentStack.js +++ b/packages/react-server/src/ReactFizzComponentStack.js @@ -43,13 +43,13 @@ export function getStackByComponentStackNode( do { switch (node.tag) { case 0: - info += describeBuiltInComponentFrame(node.type, null); + info += describeBuiltInComponentFrame(node.type); break; case 1: - info += describeFunctionComponentFrame(node.type, null); + info += describeFunctionComponentFrame(node.type); break; case 2: - info += describeClassComponentFrame(node.type, null); + info += describeClassComponentFrame(node.type); break; } // $FlowFixMe[incompatible-type] we bail out when we get a null diff --git a/packages/react-server/src/ReactFlightHooks.js b/packages/react-server/src/ReactFlightHooks.js index 75a99dc558ea5..e85e0e9ce6a60 100644 --- a/packages/react-server/src/ReactFlightHooks.js +++ b/packages/react-server/src/ReactFlightHooks.js @@ -9,7 +9,7 @@ import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes'; import type {Request} from './ReactFlightServer'; -import type {Thenable, Usable} from 'shared/ReactTypes'; +import type {Thenable, Usable, ReactComponentInfo} from 'shared/ReactTypes'; import type {ThenableState} from './ReactFlightThenable'; import { REACT_MEMO_CACHE_SENTINEL, @@ -21,6 +21,7 @@ import {isClientReference} from './ReactFlightServerConfig'; let currentRequest = null; let thenableIndexCounter = 0; let thenableState = null; +let currentComponentDebugInfo = null; export function prepareToUseHooksForRequest(request: Request) { currentRequest = request; @@ -32,9 +33,13 @@ export function resetHooksForRequest() { export function prepareToUseHooksForComponent( prevThenableState: ThenableState | null, + componentDebugInfo: null | ReactComponentInfo, ) { thenableIndexCounter = 0; thenableState = prevThenableState; + if (__DEV__) { + currentComponentDebugInfo = componentDebugInfo; + } } export function getThenableStateAfterSuspending(): ThenableState { @@ -42,6 +47,12 @@ export function getThenableStateAfterSuspending(): ThenableState { // which is not really supported anymore, it will be empty. We use the empty set as a // marker to know if this was a replay of the same component or first attempt. const state = thenableState || createThenableState(); + if (__DEV__) { + // This is a hack but we stash the debug info here so that we don't need a completely + // different data structure just for this in DEV. Not too happy about it. + (state: any)._componentDebugInfo = currentComponentDebugInfo; + currentComponentDebugInfo = null; + } thenableState = null; return state; } diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index ad0a28f37175c..a8c648881fe2c 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -58,6 +58,7 @@ import type { ReactComponentInfo, ReactAsyncInfo, } from 'shared/ReactTypes'; +import type {ReactElement} from 'shared/ReactElementType'; import type {LazyComponent} from 'react/src/ReactLazy'; import type {TemporaryReference} from './ReactFlightServerTemporaryReferences'; @@ -153,7 +154,8 @@ function patchConsole(consoleInst: typeof console, methodName: string) { // We don't currently use this id for anything but we emit it so that we can later // refer to previous logs in debug info to associate them with a component. const id = request.nextChunkId++; - emitConsoleChunk(request, id, methodName, stack, arguments); + const owner: null | ReactComponentInfo = ReactCurrentOwner.current; + emitConsoleChunk(request, id, methodName, owner, stack, arguments); } // $FlowFixMe[prop-missing] return originalMethod.apply(this, arguments); @@ -303,6 +305,7 @@ const { ReactCurrentCache, } = ReactServerSharedInternals; const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher; +const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; function throwTaintViolation(message: string) { // eslint-disable-next-line react-internal/prod-error-codes @@ -594,6 +597,7 @@ function renderFunctionComponent( key: null | string, Component: (p: Props, arg: void) => any, props: Props, + owner: null | ReactComponentInfo, ): ReactJSONValue { // Reset the task's thenable state before continuing, so that if a later // component suspends we can reuse the same task object. If the same @@ -601,6 +605,7 @@ function renderFunctionComponent( const prevThenableState = task.thenableState; task.thenableState = null; + let componentDebugInfo: null | ReactComponentInfo = null; if (__DEV__) { if (debugID === null) { // We don't have a chunk to assign debug info. We need to outline this @@ -609,22 +614,42 @@ function renderFunctionComponent( } else if (prevThenableState !== null) { // This is a replay and we've already emitted the debug info of this component // in the first pass. We skip emitting a duplicate line. + // As a hack we stashed the previous component debug info on this object in DEV. + componentDebugInfo = (prevThenableState: any)._componentDebugInfo; } else { // This is a new component in the same task so we can emit more debug info. const componentName = (Component: any).displayName || Component.name || ''; request.pendingChunks++; - emitDebugChunk(request, debugID, { + + const componentDebugID = debugID; + componentDebugInfo = { name: componentName, env: request.environmentName, - }); + owner: owner, + }; + // We outline this model eagerly so that we can refer to by reference as an owner. + // If we had a smarter way to dedupe we might not have to do this if there ends up + // being no references to this as an owner. + outlineModel(request, componentDebugInfo); + emitDebugChunk(request, componentDebugID, componentDebugInfo); } } - prepareToUseHooksForComponent(prevThenableState); + prepareToUseHooksForComponent(prevThenableState, componentDebugInfo); // The secondArg is always undefined in Server Components since refs error early. const secondArg = undefined; - let result = Component(props, secondArg); + let result; + if (__DEV__) { + ReactCurrentOwner.current = componentDebugInfo; + try { + result = Component(props, secondArg); + } finally { + ReactCurrentOwner.current = null; + } + } else { + result = Component(props, secondArg); + } if ( typeof result === 'object' && result !== null && @@ -723,9 +748,12 @@ function renderClientElement( type: any, key: null | string, props: any, + owner: null | ReactComponentInfo, // DEV-only ): ReactJSONValue { if (!enableServerComponentKeys) { - return [REACT_ELEMENT_TYPE, type, key, props]; + return __DEV__ + ? [REACT_ELEMENT_TYPE, type, key, props, owner] + : [REACT_ELEMENT_TYPE, type, key, props]; } // We prepend the terminal client element that actually gets serialized with // the keys of any Server Components which are not serialized. @@ -735,7 +763,9 @@ function renderClientElement( } else if (keyPath !== null) { key = keyPath + ',' + key; } - const element = [REACT_ELEMENT_TYPE, type, key, props]; + const element = __DEV__ + ? [REACT_ELEMENT_TYPE, type, key, props, owner] + : [REACT_ELEMENT_TYPE, type, key, props]; if (task.implicitSlot && key !== null) { // The root Server Component had no key so it was in an implicit slot. // If we had a key lower, it would end up in that slot with an explicit key. @@ -781,6 +811,7 @@ function renderElement( key: null | string, ref: mixed, props: any, + owner: null | ReactComponentInfo, // DEV only ): ReactJSONValue { if (ref !== null && ref !== undefined) { // When the ref moves to the regular props object this will implicitly @@ -801,13 +832,13 @@ function renderElement( if (typeof type === 'function') { if (isClientReference(type) || isTemporaryReference(type)) { // This is a reference to a Client Component. - return renderClientElement(task, type, key, props); + return renderClientElement(task, type, key, props, owner); } // This is a Server Component. - return renderFunctionComponent(request, task, key, type, props); + return renderFunctionComponent(request, task, key, type, props, owner); } else if (typeof type === 'string') { // This is a host element. E.g. HTML. - return renderClientElement(task, type, key, props); + return renderClientElement(task, type, key, props, owner); } else if (typeof type === 'symbol') { if (type === REACT_FRAGMENT_TYPE && key === null) { // For key-less fragments, we add a small optimization to avoid serializing @@ -828,24 +859,39 @@ function renderElement( } // This might be a built-in React component. We'll let the client decide. // Any built-in works as long as its props are serializable. - return renderClientElement(task, type, key, props); + return renderClientElement(task, type, key, props, owner); } else if (type != null && typeof type === 'object') { if (isClientReference(type)) { // This is a reference to a Client Component. - return renderClientElement(task, type, key, props); + return renderClientElement(task, type, key, props, owner); } switch (type.$$typeof) { case REACT_LAZY_TYPE: { const payload = type._payload; const init = type._init; const wrappedType = init(payload); - return renderElement(request, task, wrappedType, key, ref, props); + return renderElement( + request, + task, + wrappedType, + key, + ref, + props, + owner, + ); } case REACT_FORWARD_REF_TYPE: { - return renderFunctionComponent(request, task, key, type.render, props); + return renderFunctionComponent( + request, + task, + key, + type.render, + props, + owner, + ); } case REACT_MEMO_TYPE: { - return renderElement(request, task, type.type, key, ref, props); + return renderElement(request, task, type.type, key, ref, props, owner); } } } @@ -1356,7 +1402,7 @@ function renderModelDestructive( writtenObjects.set((value: any).props, NEVER_OUTLINED); } - const element: React$Element = (value: any); + const element: ReactElement = (value: any); if (__DEV__) { const debugInfo: ?ReactDebugInfo = (value: any)._debugInfo; @@ -1394,6 +1440,7 @@ function renderModelDestructive( element.key, ref, props, + __DEV__ ? element._owner : null, ); } case REACT_LAZY_TYPE: { @@ -1904,8 +1951,27 @@ function emitDebugChunk( ); } + // We use the console encoding so that we can dedupe objects but don't necessarily + // use the full serialization that requires a task. + const counter = {objectCount: 0}; + function replacer( + this: + | {+[key: string | number]: ReactClientValue} + | $ReadOnlyArray, + parentPropertyName: string, + value: ReactClientValue, + ): ReactJSONValue { + return renderConsoleValue( + request, + counter, + this, + parentPropertyName, + value, + ); + } + // $FlowFixMe[incompatible-type] stringify can return null - const json: string = stringify(debugInfo); + const json: string = stringify(debugInfo, replacer); const row = serializeRowHeader('D', id) + json + '\n'; const processedChunk = stringToChunk(row); request.completedRegularChunks.push(processedChunk); @@ -2207,6 +2273,7 @@ function emitConsoleChunk( request: Request, id: number, methodName: string, + owner: null | ReactComponentInfo, stackTrace: string, args: Array, ): void { @@ -2241,7 +2308,7 @@ function emitConsoleChunk( // TODO: Don't double badge if this log came from another Flight Client. const env = request.environmentName; - const payload = [methodName, stackTrace, env]; + const payload = [methodName, stackTrace, owner, env]; // $FlowFixMe[method-unbinding] payload.push.apply(payload, args); // $FlowFixMe[incompatible-type] stringify can return null diff --git a/packages/react/src/__tests__/ReactFetch-test.js b/packages/react/src/__tests__/ReactFetch-test.js index cd658066c8ca6..e612bdf6a9780 100644 --- a/packages/react/src/__tests__/ReactFetch-test.js +++ b/packages/react/src/__tests__/ReactFetch-test.js @@ -86,7 +86,7 @@ describe('ReactFetch', () => { const promise = render(Component); expect(await promise).toMatchInlineSnapshot(`"GET world []"`); expect(promise._debugInfo).toEqual( - __DEV__ ? [{name: 'Component', env: 'Server'}] : undefined, + __DEV__ ? [{name: 'Component', env: 'Server', owner: null}] : undefined, ); expect(fetchCount).toBe(1); }); diff --git a/packages/react/src/jsx/ReactJSXElement.js b/packages/react/src/jsx/ReactJSXElement.js index d20c1d4a25b56..0b8fb2f149c1e 100644 --- a/packages/react/src/jsx/ReactJSXElement.js +++ b/packages/react/src/jsx/ReactJSXElement.js @@ -1008,13 +1008,17 @@ function validateExplicitKey(element, parentType) { let childOwner = ''; if ( element && - element._owner && + element._owner != null && element._owner !== ReactCurrentOwner.current ) { + let ownerName = null; + if (typeof element._owner.tag === 'number') { + ownerName = getComponentNameFromType(element._owner.type); + } else if (typeof element._owner.name === 'string') { + ownerName = element._owner.name; + } // Give the component that originally created this child. - childOwner = ` It was passed a child from ${getComponentNameFromType( - element._owner.type, - )}.`; + childOwner = ` It was passed a child from ${ownerName}.`; } setCurrentlyValidatingElement(element); diff --git a/packages/shared/ReactComponentStackFrame.js b/packages/shared/ReactComponentStackFrame.js index c928f191d7b79..b103b760e9954 100644 --- a/packages/shared/ReactComponentStackFrame.js +++ b/packages/shared/ReactComponentStackFrame.js @@ -26,10 +26,7 @@ import ReactSharedInternals from 'shared/ReactSharedInternals'; const {ReactCurrentDispatcher} = ReactSharedInternals; let prefix; -export function describeBuiltInComponentFrame( - name: string, - ownerFn: void | null | Function, -): string { +export function describeBuiltInComponentFrame(name: string): string { if (enableComponentStackLocations) { if (prefix === undefined) { // Extract the VM specific prefix used by each line. @@ -43,19 +40,12 @@ export function describeBuiltInComponentFrame( // We use the prefix to ensure our stacks line up with native stack frames. return '\n' + prefix + name; } else { - let ownerName = null; - if (__DEV__ && ownerFn) { - ownerName = ownerFn.displayName || ownerFn.name || null; - } - return describeComponentFrame(name, ownerName); + return describeComponentFrame(name); } } export function describeDebugInfoFrame(name: string, env: ?string): string { - return describeBuiltInComponentFrame( - name + (env ? ' (' + env + ')' : ''), - null, - ); + return describeBuiltInComponentFrame(name + (env ? ' (' + env + ')' : '')); } let reentry = false; @@ -298,29 +288,19 @@ export function describeNativeComponentFrame( return syntheticFrame; } -function describeComponentFrame(name: null | string, ownerName: null | string) { - let sourceInfo = ''; - if (ownerName) { - sourceInfo = ' (created by ' + ownerName + ')'; - } - return '\n in ' + (name || 'Unknown') + sourceInfo; +function describeComponentFrame(name: null | string) { + return '\n in ' + (name || 'Unknown'); } -export function describeClassComponentFrame( - ctor: Function, - ownerFn: void | null | Function, -): string { +export function describeClassComponentFrame(ctor: Function): string { if (enableComponentStackLocations) { return describeNativeComponentFrame(ctor, true); } else { - return describeFunctionComponentFrame(ctor, ownerFn); + return describeFunctionComponentFrame(ctor); } } -export function describeFunctionComponentFrame( - fn: Function, - ownerFn: void | null | Function, -): string { +export function describeFunctionComponentFrame(fn: Function): string { if (enableComponentStackLocations) { return describeNativeComponentFrame(fn, false); } else { @@ -328,11 +308,7 @@ export function describeFunctionComponentFrame( return ''; } const name = fn.displayName || fn.name || null; - let ownerName = null; - if (__DEV__ && ownerFn) { - ownerName = ownerFn.displayName || ownerFn.name || null; - } - return describeComponentFrame(name, ownerName); + return describeComponentFrame(name); } } @@ -341,10 +317,7 @@ function shouldConstruct(Component: Function) { return !!(prototype && prototype.isReactComponent); } -export function describeUnknownElementTypeFrameInDEV( - type: any, - ownerFn: void | null | Function, -): string { +export function describeUnknownElementTypeFrameInDEV(type: any): string { if (!__DEV__) { return ''; } @@ -355,32 +328,32 @@ export function describeUnknownElementTypeFrameInDEV( if (enableComponentStackLocations) { return describeNativeComponentFrame(type, shouldConstruct(type)); } else { - return describeFunctionComponentFrame(type, ownerFn); + return describeFunctionComponentFrame(type); } } if (typeof type === 'string') { - return describeBuiltInComponentFrame(type, ownerFn); + return describeBuiltInComponentFrame(type); } switch (type) { case REACT_SUSPENSE_TYPE: - return describeBuiltInComponentFrame('Suspense', ownerFn); + return describeBuiltInComponentFrame('Suspense'); case REACT_SUSPENSE_LIST_TYPE: - return describeBuiltInComponentFrame('SuspenseList', ownerFn); + return describeBuiltInComponentFrame('SuspenseList'); } if (typeof type === 'object') { switch (type.$$typeof) { case REACT_FORWARD_REF_TYPE: - return describeFunctionComponentFrame(type.render, ownerFn); + return describeFunctionComponentFrame(type.render); case REACT_MEMO_TYPE: // Memo may contain any component type so we recursively resolve it. - return describeUnknownElementTypeFrameInDEV(type.type, ownerFn); + return describeUnknownElementTypeFrameInDEV(type.type); case REACT_LAZY_TYPE: { const lazyComponent: LazyComponent = (type: any); const payload = lazyComponent._payload; const init = lazyComponent._init; try { // Lazy may contain any component type so we recursively resolve it. - return describeUnknownElementTypeFrameInDEV(init(payload), ownerFn); + return describeUnknownElementTypeFrameInDEV(init(payload)); } catch (x) {} } } diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 59f3362e0cf8c..1c2ea36054676 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -181,6 +181,7 @@ export type Awaited = T extends null | void export type ReactComponentInfo = { +name?: string, +env?: string, + +owner?: null | ReactComponentInfo, }; export type ReactAsyncInfo = {