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.
This commit is contained in:
Sebastian Markbåge
2024-04-05 12:48:52 -04:00
committed by Rick Hanlon
parent ef3730f4bd
commit ab711223d2
20 changed files with 291 additions and 158 deletions
+18 -7
View File
@@ -484,6 +484,7 @@ function createElement(
type: mixed,
key: mixed,
props: mixed,
owner: null | ReactComponentInfo, // DEV-only
): React$Element<any> {
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;
}
+53 -5
View File
@@ -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(<span>Hello, Seb</span>);
});
});
@@ -101,6 +101,7 @@ describe('component stack', () => {
{
name: 'ServerComponent',
env: 'Server',
owner: null,
},
];
const Parent = () => ChildPromise;
@@ -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) {}
@@ -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:
+35 -18
View File
@@ -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<SerializedElement> = [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<SerializedElement>);
let owner: null | Fiber = _debugOwner;
while (owner !== null) {
owners.push(fiberToSerializedElement(owner));
owner = owner._debugOwner || null;
let owners: null | Array<SerializedElement> = 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;
}
}
@@ -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.
}
}
}
+3 -3
View File
@@ -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;
+4 -3
View File
@@ -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 + '`.';
}
+7 -12
View File
@@ -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 '';
}
+2 -1
View File
@@ -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,
@@ -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<any>) {
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) {
@@ -290,7 +290,7 @@ describe('ReactFlightDOMEdge', () => {
<ServerComponent recurse={20} />,
);
const serializedContent = await readResult(stream);
const expectedDebugInfoSize = __DEV__ ? 42 * 20 : 0;
const expectedDebugInfoSize = __DEV__ ? 64 * 20 : 0;
expect(serializedContent.length).toBeLessThan(150 + expectedDebugInfoSize);
});
+3 -3
View File
@@ -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
+12 -1
View File
@@ -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;
}
+85 -18
View File
@@ -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<Props>(
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<Props>(
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<Props>(
} 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<any> = (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<ReactClientValue>,
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<any>,
): 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
@@ -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);
});
+8 -4
View File
@@ -1051,13 +1051,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);
+17 -44
View File
@@ -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<any, any> = (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) {}
}
}
+1
View File
@@ -181,6 +181,7 @@ export type Awaited<T> = T extends null | void
export type ReactComponentInfo = {
+name?: string,
+env?: string,
+owner?: null | ReactComponentInfo,
};
export type ReactAsyncInfo = {