@samitouri / QOS-React-2 / commits / f33a6b69c6

Track Owner for Server Components in DEV (#28753)

This implements the concept of a DEV-only "owner" for Server Components. The owner concept isn't really super useful. We barely use it anymore, but we do have it as a concept in DevTools in a couple of cases so this adds it for parity. However, this is mainly interesting because it could be used to wire up future owner-based stacks. I do this by outlining the DebugInfo for a Server Component (ReactComponentInfo). Then I just rely on Flight deduping to refer to that. I refer to the same thing by referential equality so that we can associate a Server Component parent in DebugInfo with an owner. If you suspend and replay a Server Component, we have to restore the same owner. To do that, I did a little ugly hack and stashed it on the thenable state object. Felt unnecessarily complicated to add a stateful wrapper for this one dev-only case. The owner could really be anything since it could be coming from a different implementation. Because this is the first time we have an owner other than Fiber, I have to fix up a bunch of places that assumes Fiber. I mainly did the `typeof owner.tag === 'number'` to assume it's a Fiber for now. This also doesn't actually add it to DevTools / RN Inspector yet. I just ignore them there for now. Because Server Components can be async the owner isn't tracked after an await. We need per-component AsyncLocalStorage for that. This can be done in a follow up.

Sebastian Markbåge committed Apr 5, 2024 at 12:48 UTC f33a6b69c6cb406ea0cc51d07bc4d3fd2d8d8744
20 files changed +291 -158
packages/react-client/src/ReactFlightClient.js
+18 -7
@@ -484,6 +484,7 @@ function createElement(
484 type: mixed,
485 key: mixed,
486 props: mixed,
487 + owner: null | ReactComponentInfo, // DEV-only
488 ): React$Element<any> {
489 let element: any;
490 if (__DEV__ && enableRefAsProp) {
@@ -493,7 +494,7 @@ function createElement(
494 type,
495 key,
496 props,
496 - _owner: null,
497 + _owner: owner,
498 }: any);
499 Object.defineProperty(element, 'ref', {
500 enumerable: false,
@@ -520,7 +521,7 @@ function createElement(
521 props,
522
523 // Record the component responsible for creating this element.
523 - _owner: null,
524 + _owner: owner,
525 }: any);
526 }
527
@@ -854,7 +855,12 @@ function parseModelTuple(
855 if (tuple[0] === REACT_ELEMENT_TYPE) {
856 // TODO: Consider having React just directly accept these arrays as elements.
857 // Or even change the ReactElement type to be an array.
857 - return createElement(tuple[1], tuple[2], tuple[3]);
858 + return createElement(
859 + tuple[1],
860 + tuple[2],
861 + tuple[3],
862 + __DEV__ ? (tuple: any)[4] : null,
863 + );
864 }
865 return value;
866 }
@@ -1132,12 +1138,14 @@ function resolveConsoleEntry(
1138 );
1139 }
1140
1135 - const payload: [string, string, string, mixed] = parseModel(response, value);
1141 + const payload: [string, string, null | ReactComponentInfo, string, mixed] =
1142 + parseModel(response, value);
1143 const methodName = payload[0];
1144 // TODO: Restore the fake stack before logging.
1145 // const stackTrace = payload[1];
1139 - const env = payload[2];
1140 - const args = payload.slice(3);
1146 + // const owner = payload[2];
1147 + const env = payload[3];
1148 + const args = payload.slice(4);
1149 printToConsole(methodName, args, env);
1150 }
1151
@@ -1286,7 +1294,10 @@ function processFullRow(
1294 }
1295 case 68 /* "D" */: {
1296 if (__DEV__) {
1289 - const debugInfo = JSON.parse(row);
1297 + const debugInfo: ReactComponentInfo | ReactAsyncInfo = parseModel(
1298 + response,
1299 + row,
1300 + );
1301 resolveDebugInfo(response, id, debugInfo);
1302 return;
1303 }
packages/react-client/src/__tests__/ReactFlight-test.js
+53 -5
@@ -214,7 +214,7 @@ describe('ReactFlight', () => {
214 const rootModel = await ReactNoopFlightClient.read(transport);
215 const greeting = rootModel.greeting;
216 expect(greeting._debugInfo).toEqual(
217 - __DEV__ ? [{name: 'Greeting', env: 'Server'}] : undefined,
217 + __DEV__ ? [{name: 'Greeting', env: 'Server', owner: null}] : undefined,
218 );
219 ReactNoop.render(greeting);
220 });
@@ -241,7 +241,7 @@ describe('ReactFlight', () => {
241 await act(async () => {
242 const promise = ReactNoopFlightClient.read(transport);
243 expect(promise._debugInfo).toEqual(
244 - __DEV__ ? [{name: 'Greeting', env: 'Server'}] : undefined,
244 + __DEV__ ? [{name: 'Greeting', env: 'Server', owner: null}] : undefined,
245 );
246 ReactNoop.render(await promise);
247 });
@@ -2072,19 +2072,21 @@ describe('ReactFlight', () => {
2072 await act(async () => {
2073 const promise = ReactNoopFlightClient.read(transport);
2074 expect(promise._debugInfo).toEqual(
2075 - __DEV__ ? [{name: 'ServerComponent', env: 'Server'}] : undefined,
2075 + __DEV__
2076 + ? [{name: 'ServerComponent', env: 'Server', owner: null}]
2077 + : undefined,
2078 );
2079 const result = await promise;
2080 const thirdPartyChildren = await result.props.children[1];
2081 // We expect the debug info to be transferred from the inner stream to the outer.
2082 expect(thirdPartyChildren[0]._debugInfo).toEqual(
2083 __DEV__
2082 - ? [{name: 'ThirdPartyComponent', env: 'third-party'}]
2084 + ? [{name: 'ThirdPartyComponent', env: 'third-party', owner: null}]
2085 : undefined,
2086 );
2087 expect(thirdPartyChildren[1]._debugInfo).toEqual(
2088 __DEV__
2087 - ? [{name: 'ThirdPartyLazyComponent', env: 'third-party'}]
2089 + ? [{name: 'ThirdPartyLazyComponent', env: 'third-party', owner: null}]
2090 : undefined,
2091 );
2092 ReactNoop.render(result);
@@ -2145,4 +2147,50 @@ describe('ReactFlight', () => {
2147 expect(loggedFn).not.toBe(foo);
2148 expect(loggedFn.toString()).toBe(foo.toString());
2149 });
2150 +
2151 + it('uses the server component debug info as the element owner in DEV', async () => {
2152 + function Container({children}) {
2153 + return children;
2154 + }
2155 +
2156 + function Greeting({firstName}) {
2157 + // We can't use JSX here because it'll use the Client React.
2158 + return ReactServer.createElement(
2159 + Container,
2160 + null,
2161 + ReactServer.createElement('span', null, 'Hello, ', firstName),
2162 + );
2163 + }
2164 +
2165 + const model = {
2166 + greeting: ReactServer.createElement(Greeting, {firstName: 'Seb'}),
2167 + };
2168 +
2169 + const transport = ReactNoopFlightServer.render(model);
2170 +
2171 + await act(async () => {
2172 + const rootModel = await ReactNoopFlightClient.read(transport);
2173 + const greeting = rootModel.greeting;
2174 + // We've rendered down to the span.
2175 + expect(greeting.type).toBe('span');
2176 + if (__DEV__) {
2177 + const greetInfo = {name: 'Greeting', env: 'Server', owner: null};
2178 + expect(greeting._debugInfo).toEqual([
2179 + greetInfo,
2180 + {name: 'Container', env: 'Server', owner: greetInfo},
2181 + ]);
2182 + // The owner that created the span was the outer server component.
2183 + // We expect the debug info to be referentially equal to the owner.
2184 + expect(greeting._owner).toBe(greeting._debugInfo[0]);
2185 + } else {
2186 + expect(greeting._debugInfo).toBe(undefined);
2187 + expect(greeting._owner).toBe(
2188 + gate(flags => flags.disableStringRefs) ? undefined : null,
2189 + );
2190 + }
2191 + ReactNoop.render(greeting);
2192 + });
2193 +
2194 + expect(ReactNoop).toMatchRenderedOutput(<span>Hello, Seb</span>);
2195 + });
2196 });
packages/react-devtools-shared/src/__tests__/componentStacks-test.js
+1
@@ -101,6 +101,7 @@ describe('component stack', () => {
101 {
102 name: 'ServerComponent',
103 env: 'Server',
104 + owner: null,
105 },
106 ];
107 const Parent = () => ChildPromise;
packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js
+5 -17
@@ -33,10 +33,7 @@ import {
33 import {disableLogs, reenableLogs} from './DevToolsConsolePatching';
34
35 let prefix;
36 -export function describeBuiltInComponentFrame(
37 - name: string,
38 - ownerFn: void | null | Function,
39 -): string {
36 +export function describeBuiltInComponentFrame(name: string): string {
37 if (prefix === undefined) {
38 // Extract the VM specific prefix used by each line.
39 try {
@@ -51,10 +48,7 @@ export function describeBuiltInComponentFrame(
48 }
49
50 export function describeDebugInfoFrame(name: string, env: ?string): string {
54 - return describeBuiltInComponentFrame(
55 - name + (env ? ' (' + env + ')' : ''),
56 - null,
57 - );
51 + return describeBuiltInComponentFrame(name + (env ? ' (' + env + ')' : ''));
52 }
53
54 let reentry = false;
@@ -292,7 +286,6 @@ export function describeNativeComponentFrame(
286
287 export function describeClassComponentFrame(
288 ctor: Function,
295 - ownerFn: void | null | Function,
289 currentDispatcherRef: CurrentDispatcherRef,
290 ): string {
291 return describeNativeComponentFrame(ctor, true, currentDispatcherRef);
@@ -300,7 +293,6 @@ export function describeClassComponentFrame(
293
294 export function describeFunctionComponentFrame(
295 fn: Function,
303 - ownerFn: void | null | Function,
296 currentDispatcherRef: CurrentDispatcherRef,
297 ): string {
298 return describeNativeComponentFrame(fn, false, currentDispatcherRef);
@@ -313,7 +305,6 @@ function shouldConstruct(Component: Function) {
305
306 export function describeUnknownElementTypeFrameInDEV(
307 type: any,
316 - ownerFn: void | null | Function,
308 currentDispatcherRef: CurrentDispatcherRef,
309 ): string {
310 if (!__DEV__) {
@@ -330,15 +321,15 @@ export function describeUnknownElementTypeFrameInDEV(
321 );
322 }
323 if (typeof type === 'string') {
333 - return describeBuiltInComponentFrame(type, ownerFn);
324 + return describeBuiltInComponentFrame(type);
325 }
326 switch (type) {
327 case SUSPENSE_NUMBER:
328 case SUSPENSE_SYMBOL_STRING:
338 - return describeBuiltInComponentFrame('Suspense', ownerFn);
329 + return describeBuiltInComponentFrame('Suspense');
330 case SUSPENSE_LIST_NUMBER:
331 case SUSPENSE_LIST_SYMBOL_STRING:
341 - return describeBuiltInComponentFrame('SuspenseList', ownerFn);
332 + return describeBuiltInComponentFrame('SuspenseList');
333 }
334 if (typeof type === 'object') {
335 switch (type.$$typeof) {
@@ -346,7 +337,6 @@ export function describeUnknownElementTypeFrameInDEV(
337 case FORWARD_REF_SYMBOL_STRING:
338 return describeFunctionComponentFrame(
339 type.render,
349 - ownerFn,
340 currentDispatcherRef,
341 );
342 case MEMO_NUMBER:
@@ -354,7 +344,6 @@ export function describeUnknownElementTypeFrameInDEV(
344 // Memo may contain any component type so we recursively resolve it.
345 return describeUnknownElementTypeFrameInDEV(
346 type.type,
357 - ownerFn,
347 currentDispatcherRef,
348 );
349 case LAZY_NUMBER:
@@ -366,7 +355,6 @@ export function describeUnknownElementTypeFrameInDEV(
355 // Lazy may contain any component type so we recursively resolve it.
356 return describeUnknownElementTypeFrameInDEV(
357 init(payload),
369 - ownerFn,
358 currentDispatcherRef,
359 );
360 } catch (x) {}
packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js
+4 -12
@@ -39,38 +39,30 @@ export function describeFiber(
39 ClassComponent,
40 } = workTagMap;
41
42 - const owner: null | Function = __DEV__
43 - ? workInProgress._debugOwner
44 - ? workInProgress._debugOwner.type
45 - : null
46 - : null;
42 switch (workInProgress.tag) {
43 case HostComponent:
49 - return describeBuiltInComponentFrame(workInProgress.type, owner);
44 + return describeBuiltInComponentFrame(workInProgress.type);
45 case LazyComponent:
51 - return describeBuiltInComponentFrame('Lazy', owner);
46 + return describeBuiltInComponentFrame('Lazy');
47 case SuspenseComponent:
53 - return describeBuiltInComponentFrame('Suspense', owner);
48 + return describeBuiltInComponentFrame('Suspense');
49 case SuspenseListComponent:
55 - return describeBuiltInComponentFrame('SuspenseList', owner);
50 + return describeBuiltInComponentFrame('SuspenseList');
51 case FunctionComponent:
52 case IndeterminateComponent:
53 case SimpleMemoComponent:
54 return describeFunctionComponentFrame(
55 workInProgress.type,
61 - owner,
56 currentDispatcherRef,
57 );
58 case ForwardRef:
59 return describeFunctionComponentFrame(
60 workInProgress.type.render,
67 - owner,
61 currentDispatcherRef,
62 );
63 case ClassComponent:
64 return describeClassComponentFrame(
65 workInProgress.type,
73 - owner,
66 currentDispatcherRef,
67 );
68 default:
packages/react-devtools-shared/src/backend/renderer.js
+35 -18
@@ -1952,15 +1952,24 @@ export function attach(
1952 const {key} = fiber;
1953 const displayName = getDisplayNameForFiber(fiber);
1954 const elementType = getElementTypeForFiber(fiber);
1955 - const {_debugOwner} = fiber;
1955 + const debugOwner = fiber._debugOwner;
1956
1957 // Ideally we should call getFiberIDThrows() for _debugOwner,
1958 // since owners are almost always higher in the tree (and so have already been processed),
1959 // but in some (rare) instances reported in open source, a descendant mounts before an owner.
1960 // Since this is a DEV only field it's probably okay to also just lazily generate and ID here if needed.
1961 // See https://github.com/facebook/react/issues/21445
1962 - const ownerID =
1963 - _debugOwner != null ? getOrGenerateFiberID(_debugOwner) : 0;
1962 + let ownerID: number;
1963 + if (debugOwner != null) {
1964 + if (typeof debugOwner.tag === 'number') {
1965 + ownerID = getOrGenerateFiberID((debugOwner: any));
1966 + } else {
1967 + // TODO: Track Server Component Owners.
1968 + ownerID = 0;
1969 + }
1970 + } else {
1971 + ownerID = 0;
1972 + }
1973 const parentID = parentFiber ? getFiberIDThrows(parentFiber) : 0;
1974
1975 const displayNameStringID = getStringID(displayName);
@@ -3104,15 +3113,17 @@ export function attach(
3113 return null;
3114 }
3115
3107 - const {_debugOwner} = fiber;
3108 -
3116 const owners: Array<SerializedElement> = [fiberToSerializedElement(fiber)];
3117
3111 - if (_debugOwner) {
3112 - let owner: null | Fiber = _debugOwner;
3113 - while (owner !== null) {
3114 - owners.unshift(fiberToSerializedElement(owner));
3115 - owner = owner._debugOwner || null;
3118 + let owner = fiber._debugOwner;
3119 + while (owner != null) {
3120 + if (typeof owner.tag === 'number') {
3121 + const ownerFiber: Fiber = (owner: any); // Refined
3122 + owners.unshift(fiberToSerializedElement(ownerFiber));
3123 + owner = ownerFiber._debugOwner;
3124 + } else {
3125 + // TODO: Track Server Component Owners.
3126 + break;
3127 }
3128 }
3129
@@ -3173,7 +3184,7 @@ export function attach(
3184 }
3185
3186 const {
3176 - _debugOwner,
3187 + _debugOwner: debugOwner,
3188 stateNode,
3189 key,
3190 memoizedProps,
@@ -3300,13 +3311,19 @@ export function attach(
3311 context = {value: context};
3312 }
3313
3303 - let owners = null;
3304 - if (_debugOwner) {
3305 - owners = ([]: Array<SerializedElement>);
3306 - let owner: null | Fiber = _debugOwner;
3307 - while (owner !== null) {
3308 - owners.push(fiberToSerializedElement(owner));
3309 - owner = owner._debugOwner || null;
3314 + let owners: null | Array<SerializedElement> = null;
3315 + let owner = debugOwner;
3316 + while (owner != null) {
3317 + if (typeof owner.tag === 'number') {
3318 + const ownerFiber: Fiber = (owner: any); // Refined
3319 + if (owners === null) {
3320 + owners = [];
3321 + }
3322 + owners.push(fiberToSerializedElement(ownerFiber));
3323 + owner = ownerFiber._debugOwner;
3324 + } else {
3325 + // TODO: Track Server Component Owners.
3326 + break;
3327 }
3328 }
3329
packages/react-native-renderer/src/ReactNativeFiberInspector.js
+18 -8
@@ -103,13 +103,21 @@ function getInspectorDataForInstance(
103 }
104
105 const fiber = findCurrentFiberUsingSlowPath(closestInstance);
106 + if (fiber === null) {
107 + // Might not be currently mounted.
108 + return {
109 + hierarchy: [],
110 + props: emptyObject,
111 + selectedIndex: null,
112 + componentStack: '',
113 + };
114 + }
115 const fiberHierarchy = getOwnerHierarchy(fiber);
116 const instance = lastNonHostInstance(fiberHierarchy);
117 const hierarchy = createHierarchy(fiberHierarchy);
118 const props = getHostProps(instance);
119 const selectedIndex = fiberHierarchy.indexOf(instance);
111 - const componentStack =
112 - fiber !== null ? getStackByFiberInDevAndProd(fiber) : '';
120 + const componentStack = getStackByFiberInDevAndProd(fiber);
121
122 return {
123 closestInstance: instance,
@@ -125,7 +133,7 @@ function getInspectorDataForInstance(
133 );
134 }
135
128 -function getOwnerHierarchy(instance: any) {
136 +function getOwnerHierarchy(instance: Fiber) {
137 const hierarchy: Array<$FlowFixMe> = [];
138 traverseOwnerTreeUp(hierarchy, instance);
139 return hierarchy;
@@ -143,15 +151,17 @@ function lastNonHostInstance(hierarchy) {
151 return hierarchy[0];
152 }
153
146 -// $FlowFixMe[missing-local-annot]
154 function traverseOwnerTreeUp(
155 hierarchy: Array<$FlowFixMe>,
149 - instance: any,
156 + instance: Fiber,
157 ): void {
158 if (__DEV__ || enableGetInspectorDataForInstanceInProduction) {
152 - if (instance) {
153 - hierarchy.unshift(instance);
154 - traverseOwnerTreeUp(hierarchy, instance._debugOwner);
159 + hierarchy.unshift(instance);
160 + const owner = instance._debugOwner;
161 + if (owner != null && typeof owner.tag === 'number') {
162 + traverseOwnerTreeUp(hierarchy, (owner: any));
163 + } else {
164 + // TODO: Traverse Server Components owners.
165 }
166 }
167 }
packages/react-reconciler/src/ReactCurrentFiber.js
+3 -3
@@ -11,7 +11,7 @@ import type {Fiber} from './ReactInternalTypes';
11
12 import ReactSharedInternals from 'shared/ReactSharedInternals';
13 import {getStackByFiberInDevAndProd} from './ReactFiberComponentStack';
14 -import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
14 +import {getComponentNameFromOwner} from 'react-reconciler/src/getComponentNameFromFiber';
15
16 const ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;
17
@@ -24,8 +24,8 @@ export function getCurrentFiberOwnerNameInDevOrNull(): string | null {
24 return null;
25 }
26 const owner = current._debugOwner;
27 - if (owner !== null && typeof owner !== 'undefined') {
28 - return getComponentNameFromFiber(owner);
27 + if (owner != null) {
28 + return getComponentNameFromOwner(owner);
29 }
30 }
31 return null;
packages/react-reconciler/src/ReactFiber.js
+4 -3
@@ -68,7 +68,7 @@ import {
68 TracingMarkerComponent,
69 } from './ReactWorkTags';
70 import {OffscreenVisible} from './ReactFiberActivityComponent';
71 -import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
71 +import {getComponentNameFromOwner} from 'react-reconciler/src/getComponentNameFromFiber';
72 import {isDevToolsPresent} from './ReactFiberDevToolsHook';
73 import {
74 resolveClassForHotReloading,
@@ -110,6 +110,7 @@ import {
110 attachOffscreenInstance,
111 } from './ReactFiberCommitWork';
112 import {getHostContext} from './ReactFiberHostContext';
113 +import type {ReactComponentInfo} from '../../shared/ReactTypes';
114
115 export type {Fiber};
116
@@ -475,7 +476,7 @@ export function createFiberFromTypeAndProps(
476 type: any, // React$ElementType
477 key: null | string,
478 pendingProps: any,
478 - owner: null | Fiber,
479 + owner: null | ReactComponentInfo | Fiber,
480 mode: TypeOfMode,
481 lanes: Lanes,
482 ): Fiber {
@@ -610,7 +611,7 @@ export function createFiberFromTypeAndProps(
611 "it's defined in, or you might have mixed up default and " +
612 'named imports.';
613 }
613 - const ownerName = owner ? getComponentNameFromFiber(owner) : null;
614 + const ownerName = owner ? getComponentNameFromOwner(owner) : null;
615 if (ownerName) {
616 info += '\n\nCheck the render method of `' + ownerName + '`.';
617 }
packages/react-reconciler/src/ReactFiberComponentStack.js
+7 -12
@@ -29,29 +29,24 @@ import {
29 } from 'shared/ReactComponentStackFrame';
30
31 function describeFiber(fiber: Fiber): string {
32 - const owner: null | Function = __DEV__
33 - ? fiber._debugOwner
34 - ? fiber._debugOwner.type
35 - : null
36 - : null;
32 switch (fiber.tag) {
33 case HostHoistable:
34 case HostSingleton:
35 case HostComponent:
41 - return describeBuiltInComponentFrame(fiber.type, owner);
36 + return describeBuiltInComponentFrame(fiber.type);
37 case LazyComponent:
43 - return describeBuiltInComponentFrame('Lazy', owner);
38 + return describeBuiltInComponentFrame('Lazy');
39 case SuspenseComponent:
45 - return describeBuiltInComponentFrame('Suspense', owner);
40 + return describeBuiltInComponentFrame('Suspense');
41 case SuspenseListComponent:
47 - return describeBuiltInComponentFrame('SuspenseList', owner);
42 + return describeBuiltInComponentFrame('SuspenseList');
43 case FunctionComponent:
44 case SimpleMemoComponent:
50 - return describeFunctionComponentFrame(fiber.type, owner);
45 + return describeFunctionComponentFrame(fiber.type);
46 case ForwardRef:
52 - return describeFunctionComponentFrame(fiber.type.render, owner);
47 + return describeFunctionComponentFrame(fiber.type.render);
48 case ClassComponent:
54 - return describeClassComponentFrame(fiber.type, owner);
49 + return describeClassComponentFrame(fiber.type);
50 default:
51 return '';
52 }
packages/react-reconciler/src/ReactInternalTypes.js
+2 -1
@@ -15,6 +15,7 @@ import type {
15 Usable,
16 ReactFormState,
17 Awaited,
18 + ReactComponentInfo,
19 ReactDebugInfo,
20 } from 'shared/ReactTypes';
21 import type {WorkTag} from './ReactWorkTags';
@@ -193,7 +194,7 @@ export type Fiber = {
194 // __DEV__ only
195
196 _debugInfo?: ReactDebugInfo | null,
196 - _debugOwner?: Fiber | null,
197 + _debugOwner?: ReactComponentInfo | Fiber | null,
198 _debugIsCurrentlyTiming?: boolean,
199 _debugNeedsRemount?: boolean,
200
packages/react-reconciler/src/getComponentNameFromFiber.js
+13
@@ -47,6 +47,7 @@ import {
47 } from 'react-reconciler/src/ReactWorkTags';
48 import getComponentNameFromType from 'shared/getComponentNameFromType';
49 import {REACT_STRICT_MODE_TYPE} from 'shared/ReactSymbols';
50 +import type {ReactComponentInfo} from '../../shared/ReactTypes';
51
52 // Keep in sync with shared/getComponentNameFromType
53 function getWrappedName(
@@ -66,6 +67,18 @@ function getContextName(type: ReactContext<any>) {
67 return type.displayName || 'Context';
68 }
69
70 +export function getComponentNameFromOwner(
71 + owner: Fiber | ReactComponentInfo,
72 +): string | null {
73 + if (typeof owner.tag === 'number') {
74 + return getComponentNameFromFiber((owner: any));
75 + }
76 + if (typeof owner.name === 'string') {
77 + return owner.name;
78 + }
79 + return null;
80 +}
81 +
82 export default function getComponentNameFromFiber(fiber: Fiber): string | null {
83 const {tag, type} = fiber;
84 switch (tag) {
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js
+1 -1
@@ -290,7 +290,7 @@ describe('ReactFlightDOMEdge', () => {
290 <ServerComponent recurse={20} />,
291 );
292 const serializedContent = await readResult(stream);
293 - const expectedDebugInfoSize = __DEV__ ? 42 * 20 : 0;
293 + const expectedDebugInfoSize = __DEV__ ? 64 * 20 : 0;
294 expect(serializedContent.length).toBeLessThan(150 + expectedDebugInfoSize);
295 });
296
packages/react-server/src/ReactFizzComponentStack.js
+3 -3
@@ -43,13 +43,13 @@ export function getStackByComponentStackNode(
43 do {
44 switch (node.tag) {
45 case 0:
46 - info += describeBuiltInComponentFrame(node.type, null);
46 + info += describeBuiltInComponentFrame(node.type);
47 break;
48 case 1:
49 - info += describeFunctionComponentFrame(node.type, null);
49 + info += describeFunctionComponentFrame(node.type);
50 break;
51 case 2:
52 - info += describeClassComponentFrame(node.type, null);
52 + info += describeClassComponentFrame(node.type);
53 break;
54 }
55 // $FlowFixMe[incompatible-type] we bail out when we get a null
packages/react-server/src/ReactFlightHooks.js
+12 -1
@@ -9,7 +9,7 @@
9
10 import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes';
11 import type {Request} from './ReactFlightServer';
12 -import type {Thenable, Usable} from 'shared/ReactTypes';
12 +import type {Thenable, Usable, ReactComponentInfo} from 'shared/ReactTypes';
13 import type {ThenableState} from './ReactFlightThenable';
14 import {
15 REACT_MEMO_CACHE_SENTINEL,
@@ -21,6 +21,7 @@ import {isClientReference} from './ReactFlightServerConfig';
21 let currentRequest = null;
22 let thenableIndexCounter = 0;
23 let thenableState = null;
24 +let currentComponentDebugInfo = null;
25
26 export function prepareToUseHooksForRequest(request: Request) {
27 currentRequest = request;
@@ -32,9 +33,13 @@ export function resetHooksForRequest() {
33
34 export function prepareToUseHooksForComponent(
35 prevThenableState: ThenableState | null,
36 + componentDebugInfo: null | ReactComponentInfo,
37 ) {
38 thenableIndexCounter = 0;
39 thenableState = prevThenableState;
40 + if (__DEV__) {
41 + currentComponentDebugInfo = componentDebugInfo;
42 + }
43 }
44
45 export function getThenableStateAfterSuspending(): ThenableState {
@@ -42,6 +47,12 @@ export function getThenableStateAfterSuspending(): ThenableState {
47 // which is not really supported anymore, it will be empty. We use the empty set as a
48 // marker to know if this was a replay of the same component or first attempt.
49 const state = thenableState || createThenableState();
50 + if (__DEV__) {
51 + // This is a hack but we stash the debug info here so that we don't need a completely
52 + // different data structure just for this in DEV. Not too happy about it.
53 + (state: any)._componentDebugInfo = currentComponentDebugInfo;
54 + currentComponentDebugInfo = null;
55 + }
56 thenableState = null;
57 return state;
58 }
packages/react-server/src/ReactFlightServer.js
+85 -18
@@ -58,6 +58,7 @@ import type {
58 ReactComponentInfo,
59 ReactAsyncInfo,
60 } from 'shared/ReactTypes';
61 +import type {ReactElement} from 'shared/ReactElementType';
62 import type {LazyComponent} from 'react/src/ReactLazy';
63 import type {TemporaryReference} from './ReactFlightServerTemporaryReferences';
64
@@ -153,7 +154,8 @@ function patchConsole(consoleInst: typeof console, methodName: string) {
154 // We don't currently use this id for anything but we emit it so that we can later
155 // refer to previous logs in debug info to associate them with a component.
156 const id = request.nextChunkId++;
156 - emitConsoleChunk(request, id, methodName, stack, arguments);
157 + const owner: null | ReactComponentInfo = ReactCurrentOwner.current;
158 + emitConsoleChunk(request, id, methodName, owner, stack, arguments);
159 }
160 // $FlowFixMe[prop-missing]
161 return originalMethod.apply(this, arguments);
@@ -303,6 +305,7 @@ const {
305 ReactCurrentCache,
306 } = ReactServerSharedInternals;
307 const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
308 +const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
309
310 function throwTaintViolation(message: string) {
311 // eslint-disable-next-line react-internal/prod-error-codes
@@ -594,6 +597,7 @@ function renderFunctionComponent<Props>(
597 key: null | string,
598 Component: (p: Props, arg: void) => any,
599 props: Props,
600 + owner: null | ReactComponentInfo,
601 ): ReactJSONValue {
602 // Reset the task's thenable state before continuing, so that if a later
603 // component suspends we can reuse the same task object. If the same
@@ -601,6 +605,7 @@ function renderFunctionComponent<Props>(
605 const prevThenableState = task.thenableState;
606 task.thenableState = null;
607
608 + let componentDebugInfo: null | ReactComponentInfo = null;
609 if (__DEV__) {
610 if (debugID === null) {
611 // We don't have a chunk to assign debug info. We need to outline this
@@ -609,22 +614,42 @@ function renderFunctionComponent<Props>(
614 } else if (prevThenableState !== null) {
615 // This is a replay and we've already emitted the debug info of this component
616 // in the first pass. We skip emitting a duplicate line.
617 + // As a hack we stashed the previous component debug info on this object in DEV.
618 + componentDebugInfo = (prevThenableState: any)._componentDebugInfo;
619 } else {
620 // This is a new component in the same task so we can emit more debug info.
621 const componentName =
622 (Component: any).displayName || Component.name || '';
623 request.pendingChunks++;
617 - emitDebugChunk(request, debugID, {
624 +
625 + const componentDebugID = debugID;
626 + componentDebugInfo = {
627 name: componentName,
628 env: request.environmentName,
620 - });
629 + owner: owner,
630 + };
631 + // We outline this model eagerly so that we can refer to by reference as an owner.
632 + // If we had a smarter way to dedupe we might not have to do this if there ends up
633 + // being no references to this as an owner.
634 + outlineModel(request, componentDebugInfo);
635 + emitDebugChunk(request, componentDebugID, componentDebugInfo);
636 }
637 }
638
624 - prepareToUseHooksForComponent(prevThenableState);
639 + prepareToUseHooksForComponent(prevThenableState, componentDebugInfo);
640 // The secondArg is always undefined in Server Components since refs error early.
641 const secondArg = undefined;
627 - let result = Component(props, secondArg);
642 + let result;
643 + if (__DEV__) {
644 + ReactCurrentOwner.current = componentDebugInfo;
645 + try {
646 + result = Component(props, secondArg);
647 + } finally {
648 + ReactCurrentOwner.current = null;
649 + }
650 + } else {
651 + result = Component(props, secondArg);
652 + }
653 if (
654 typeof result === 'object' &&
655 result !== null &&
@@ -723,9 +748,12 @@ function renderClientElement(
748 type: any,
749 key: null | string,
750 props: any,
751 + owner: null | ReactComponentInfo, // DEV-only
752 ): ReactJSONValue {
753 if (!enableServerComponentKeys) {
728 - return [REACT_ELEMENT_TYPE, type, key, props];
754 + return __DEV__
755 + ? [REACT_ELEMENT_TYPE, type, key, props, owner]
756 + : [REACT_ELEMENT_TYPE, type, key, props];
757 }
758 // We prepend the terminal client element that actually gets serialized with
759 // the keys of any Server Components which are not serialized.
@@ -735,7 +763,9 @@ function renderClientElement(
763 } else if (keyPath !== null) {
764 key = keyPath + ',' + key;
765 }
738 - const element = [REACT_ELEMENT_TYPE, type, key, props];
766 + const element = __DEV__
767 + ? [REACT_ELEMENT_TYPE, type, key, props, owner]
768 + : [REACT_ELEMENT_TYPE, type, key, props];
769 if (task.implicitSlot && key !== null) {
770 // The root Server Component had no key so it was in an implicit slot.
771 // If we had a key lower, it would end up in that slot with an explicit key.
@@ -781,6 +811,7 @@ function renderElement(
811 key: null | string,
812 ref: mixed,
813 props: any,
814 + owner: null | ReactComponentInfo, // DEV only
815 ): ReactJSONValue {
816 if (ref !== null && ref !== undefined) {
817 // When the ref moves to the regular props object this will implicitly
@@ -801,13 +832,13 @@ function renderElement(
832 if (typeof type === 'function') {
833 if (isClientReference(type) || isTemporaryReference(type)) {
834 // This is a reference to a Client Component.
804 - return renderClientElement(task, type, key, props);
835 + return renderClientElement(task, type, key, props, owner);
836 }
837 // This is a Server Component.
807 - return renderFunctionComponent(request, task, key, type, props);
838 + return renderFunctionComponent(request, task, key, type, props, owner);
839 } else if (typeof type === 'string') {
840 // This is a host element. E.g. HTML.
810 - return renderClientElement(task, type, key, props);
841 + return renderClientElement(task, type, key, props, owner);
842 } else if (typeof type === 'symbol') {
843 if (type === REACT_FRAGMENT_TYPE && key === null) {
844 // For key-less fragments, we add a small optimization to avoid serializing
@@ -828,24 +859,39 @@ function renderElement(
859 }
860 // This might be a built-in React component. We'll let the client decide.
861 // Any built-in works as long as its props are serializable.
831 - return renderClientElement(task, type, key, props);
862 + return renderClientElement(task, type, key, props, owner);
863 } else if (type != null && typeof type === 'object') {
864 if (isClientReference(type)) {
865 // This is a reference to a Client Component.
835 - return renderClientElement(task, type, key, props);
866 + return renderClientElement(task, type, key, props, owner);
867 }
868 switch (type.$$typeof) {
869 case REACT_LAZY_TYPE: {
870 const payload = type._payload;
871 const init = type._init;
872 const wrappedType = init(payload);
842 - return renderElement(request, task, wrappedType, key, ref, props);
873 + return renderElement(
874 + request,
875 + task,
876 + wrappedType,
877 + key,
878 + ref,
879 + props,
880 + owner,
881 + );
882 }
883 case REACT_FORWARD_REF_TYPE: {
845 - return renderFunctionComponent(request, task, key, type.render, props);
884 + return renderFunctionComponent(
885 + request,
886 + task,
887 + key,
888 + type.render,
889 + props,
890 + owner,
891 + );
892 }
893 case REACT_MEMO_TYPE: {
848 - return renderElement(request, task, type.type, key, ref, props);
894 + return renderElement(request, task, type.type, key, ref, props, owner);
895 }
896 }
897 }
@@ -1356,7 +1402,7 @@ function renderModelDestructive(
1402 writtenObjects.set((value: any).props, NEVER_OUTLINED);
1403 }
1404
1359 - const element: React$Element<any> = (value: any);
1405 + const element: ReactElement = (value: any);
1406
1407 if (__DEV__) {
1408 const debugInfo: ?ReactDebugInfo = (value: any)._debugInfo;
@@ -1394,6 +1440,7 @@ function renderModelDestructive(
1440 element.key,
1441 ref,
1442 props,
1443 + __DEV__ ? element._owner : null,
1444 );
1445 }
1446 case REACT_LAZY_TYPE: {
@@ -1904,8 +1951,27 @@ function emitDebugChunk(
1951 );
1952 }
1953
1954 + // We use the console encoding so that we can dedupe objects but don't necessarily
1955 + // use the full serialization that requires a task.
1956 + const counter = {objectCount: 0};
1957 + function replacer(
1958 + this:
1959 + | {+[key: string | number]: ReactClientValue}
1960 + | $ReadOnlyArray<ReactClientValue>,
1961 + parentPropertyName: string,
1962 + value: ReactClientValue,
1963 + ): ReactJSONValue {
1964 + return renderConsoleValue(
1965 + request,
1966 + counter,
1967 + this,
1968 + parentPropertyName,
1969 + value,
1970 + );
1971 + }
1972 +
1973 // $FlowFixMe[incompatible-type] stringify can return null
1908 - const json: string = stringify(debugInfo);
1974 + const json: string = stringify(debugInfo, replacer);
1975 const row = serializeRowHeader('D', id) + json + '\n';
1976 const processedChunk = stringToChunk(row);
1977 request.completedRegularChunks.push(processedChunk);
@@ -2207,6 +2273,7 @@ function emitConsoleChunk(
2273 request: Request,
2274 id: number,
2275 methodName: string,
2276 + owner: null | ReactComponentInfo,
2277 stackTrace: string,
2278 args: Array<any>,
2279 ): void {
@@ -2241,7 +2308,7 @@ function emitConsoleChunk(
2308
2309 // TODO: Don't double badge if this log came from another Flight Client.
2310 const env = request.environmentName;
2244 - const payload = [methodName, stackTrace, env];
2311 + const payload = [methodName, stackTrace, owner, env];
2312 // $FlowFixMe[method-unbinding]
2313 payload.push.apply(payload, args);
2314 // $FlowFixMe[incompatible-type] stringify can return null
packages/react/src/__tests__/ReactFetch-test.js
+1 -1
@@ -86,7 +86,7 @@ describe('ReactFetch', () => {
86 const promise = render(Component);
87 expect(await promise).toMatchInlineSnapshot(`"GET world []"`);
88 expect(promise._debugInfo).toEqual(
89 - __DEV__ ? [{name: 'Component', env: 'Server'}] : undefined,
89 + __DEV__ ? [{name: 'Component', env: 'Server', owner: null}] : undefined,
90 );
91 expect(fetchCount).toBe(1);
92 });
packages/react/src/jsx/ReactJSXElement.js
+8 -4
@@ -1051,13 +1051,17 @@ function validateExplicitKey(element, parentType) {
1051 let childOwner = '';
1052 if (
1053 element &&
1054 - element._owner &&
1054 + element._owner != null &&
1055 element._owner !== ReactCurrentOwner.current
1056 ) {
1057 + let ownerName = null;
1058 + if (typeof element._owner.tag === 'number') {
1059 + ownerName = getComponentNameFromType(element._owner.type);
1060 + } else if (typeof element._owner.name === 'string') {
1061 + ownerName = element._owner.name;
1062 + }
1063 // Give the component that originally created this child.
1058 - childOwner = ` It was passed a child from ${getComponentNameFromType(
1059 - element._owner.type,
1060 - )}.`;
1064 + childOwner = ` It was passed a child from ${ownerName}.`;
1065 }
1066
1067 setCurrentlyValidatingElement(element);
packages/shared/ReactComponentStackFrame.js
+17 -44
@@ -26,10 +26,7 @@ import ReactSharedInternals from 'shared/ReactSharedInternals';
26 const {ReactCurrentDispatcher} = ReactSharedInternals;
27
28 let prefix;
29 -export function describeBuiltInComponentFrame(
30 - name: string,
31 - ownerFn: void | null | Function,
32 -): string {
29 +export function describeBuiltInComponentFrame(name: string): string {
30 if (enableComponentStackLocations) {
31 if (prefix === undefined) {
32 // Extract the VM specific prefix used by each line.
@@ -43,19 +40,12 @@ export function describeBuiltInComponentFrame(
40 // We use the prefix to ensure our stacks line up with native stack frames.
41 return '\n' + prefix + name;
42 } else {
46 - let ownerName = null;
47 - if (__DEV__ && ownerFn) {
48 - ownerName = ownerFn.displayName || ownerFn.name || null;
49 - }
50 - return describeComponentFrame(name, ownerName);
43 + return describeComponentFrame(name);
44 }
45 }
46
47 export function describeDebugInfoFrame(name: string, env: ?string): string {
55 - return describeBuiltInComponentFrame(
56 - name + (env ? ' (' + env + ')' : ''),
57 - null,
58 - );
48 + return describeBuiltInComponentFrame(name + (env ? ' (' + env + ')' : ''));
49 }
50
51 let reentry = false;
@@ -298,29 +288,19 @@ export function describeNativeComponentFrame(
288 return syntheticFrame;
289 }
290
301 -function describeComponentFrame(name: null | string, ownerName: null | string) {
302 - let sourceInfo = '';
303 - if (ownerName) {
304 - sourceInfo = ' (created by ' + ownerName + ')';
305 - }
306 - return '\n in ' + (name || 'Unknown') + sourceInfo;
291 +function describeComponentFrame(name: null | string) {
292 + return '\n in ' + (name || 'Unknown');
293 }
294
309 -export function describeClassComponentFrame(
310 - ctor: Function,
311 - ownerFn: void | null | Function,
312 -): string {
295 +export function describeClassComponentFrame(ctor: Function): string {
296 if (enableComponentStackLocations) {
297 return describeNativeComponentFrame(ctor, true);
298 } else {
316 - return describeFunctionComponentFrame(ctor, ownerFn);
299 + return describeFunctionComponentFrame(ctor);
300 }
301 }
302
320 -export function describeFunctionComponentFrame(
321 - fn: Function,
322 - ownerFn: void | null | Function,
323 -): string {
303 +export function describeFunctionComponentFrame(fn: Function): string {
304 if (enableComponentStackLocations) {
305 return describeNativeComponentFrame(fn, false);
306 } else {
@@ -328,11 +308,7 @@ export function describeFunctionComponentFrame(
308 return '';
309 }
310 const name = fn.displayName || fn.name || null;
331 - let ownerName = null;
332 - if (__DEV__ && ownerFn) {
333 - ownerName = ownerFn.displayName || ownerFn.name || null;
334 - }
335 - return describeComponentFrame(name, ownerName);
311 + return describeComponentFrame(name);
312 }
313 }
314
@@ -341,10 +317,7 @@ function shouldConstruct(Component: Function) {
317 return !!(prototype && prototype.isReactComponent);
318 }
319
344 -export function describeUnknownElementTypeFrameInDEV(
345 - type: any,
346 - ownerFn: void | null | Function,
347 -): string {
320 +export function describeUnknownElementTypeFrameInDEV(type: any): string {
321 if (!__DEV__) {
322 return '';
323 }
@@ -355,32 +328,32 @@ export function describeUnknownElementTypeFrameInDEV(
328 if (enableComponentStackLocations) {
329 return describeNativeComponentFrame(type, shouldConstruct(type));
330 } else {
358 - return describeFunctionComponentFrame(type, ownerFn);
331 + return describeFunctionComponentFrame(type);
332 }
333 }
334 if (typeof type === 'string') {
362 - return describeBuiltInComponentFrame(type, ownerFn);
335 + return describeBuiltInComponentFrame(type);
336 }
337 switch (type) {
338 case REACT_SUSPENSE_TYPE:
366 - return describeBuiltInComponentFrame('Suspense', ownerFn);
339 + return describeBuiltInComponentFrame('Suspense');
340 case REACT_SUSPENSE_LIST_TYPE:
368 - return describeBuiltInComponentFrame('SuspenseList', ownerFn);
341 + return describeBuiltInComponentFrame('SuspenseList');
342 }
343 if (typeof type === 'object') {
344 switch (type.$$typeof) {
345 case REACT_FORWARD_REF_TYPE:
373 - return describeFunctionComponentFrame(type.render, ownerFn);
346 + return describeFunctionComponentFrame(type.render);
347 case REACT_MEMO_TYPE:
348 // Memo may contain any component type so we recursively resolve it.
376 - return describeUnknownElementTypeFrameInDEV(type.type, ownerFn);
349 + return describeUnknownElementTypeFrameInDEV(type.type);
350 case REACT_LAZY_TYPE: {
351 const lazyComponent: LazyComponent<any, any> = (type: any);
352 const payload = lazyComponent._payload;
353 const init = lazyComponent._init;
354 try {
355 // Lazy may contain any component type so we recursively resolve it.
383 - return describeUnknownElementTypeFrameInDEV(init(payload), ownerFn);
356 + return describeUnknownElementTypeFrameInDEV(init(payload));
357 } catch (x) {}
358 }
359 }
packages/shared/ReactTypes.js
+1
@@ -181,6 +181,7 @@ export type Awaited<T> = T extends null | void
181 export type ReactComponentInfo = {
182 +name?: string,
183 +env?: string,
184 + +owner?: null | ReactComponentInfo,
185 };
186
187 export type ReactAsyncInfo = {