Skip to content

Commit ccc880a

Browse files
committed
Implement component stacks
This uses a reverse linked list in DEV-only to keep track of where we're currently executing.
1 parent 86715ef commit ccc880a

File tree

2 files changed

+166
-4
lines changed

2 files changed

+166
-4
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import {
11+
describeBuiltInComponentFrame,
12+
describeFunctionComponentFrame,
13+
describeClassComponentFrame,
14+
} from 'shared/ReactComponentStackFrame';
15+
16+
// DEV-only reverse linked list representing the current component stack
17+
type BuiltInComponentStackNode = {
18+
tag: 0,
19+
parent: null | ComponentStackNode,
20+
type: string,
21+
};
22+
type FunctionComponentStackNode = {
23+
tag: 1,
24+
parent: null | ComponentStackNode,
25+
type: Function,
26+
};
27+
type ClassComponentStackNode = {
28+
tag: 2,
29+
parent: null | ComponentStackNode,
30+
type: Function,
31+
};
32+
export type ComponentStackNode =
33+
| BuiltInComponentStackNode
34+
| FunctionComponentStackNode
35+
| ClassComponentStackNode;
36+
37+
export function getStackByComponentStackNode(
38+
componentStack: ComponentStackNode,
39+
): string {
40+
try {
41+
let info = '';
42+
let node = componentStack;
43+
do {
44+
switch (node.tag) {
45+
case 0:
46+
info += describeBuiltInComponentFrame(node.type, null, null);
47+
break;
48+
case 1:
49+
info += describeFunctionComponentFrame(node.type, null, null);
50+
break;
51+
case 2:
52+
info += describeClassComponentFrame(node.type, null, null);
53+
break;
54+
}
55+
node = node.parent;
56+
} while (node);
57+
return info;
58+
} catch (x) {
59+
return '\nError generating stack: ' + x.message + '\n' + x.stack;
60+
}
61+
}

packages/react-server/src/ReactFizzServer.js

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type {
2424
FormatContext,
2525
} from './ReactServerFormatConfig';
2626
import type {ContextSnapshot} from './ReactFizzNewContext';
27+
import type {ComponentStackNode} from './ReactFizzComponentStack';
2728

2829
import {
2930
scheduleWork,
@@ -77,6 +78,7 @@ import {
7778
currentResponseState,
7879
setCurrentResponseState,
7980
} from './ReactFizzHooks';
81+
import {getStackByComponentStackNode} from './ReactFizzComponentStack';
8082

8183
import {
8284
getIteratorFn,
@@ -110,6 +112,7 @@ import invariant from 'shared/invariant';
110112
import isArray from 'shared/isArray';
111113

112114
const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
115+
const ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;
113116

114117
type LegacyContext = {
115118
[key: string]: any,
@@ -135,6 +138,7 @@ type Task = {
135138
legacyContext: LegacyContext, // the current legacy context that this task is executing in
136139
context: ContextSnapshot, // the current new context that this task is executing in
137140
assignID: null | SuspenseBoundaryID, // id to assign to the content
141+
componentStack: null | ComponentStackNode, // DEV-only component stack
138142
};
139143

140144
const PENDING = 0;
@@ -299,7 +303,7 @@ function createTask(
299303
} else {
300304
blockedBoundary.pendingTasks++;
301305
}
302-
const task = {
306+
const task: Task = ({
303307
node,
304308
ping: () => pingTask(request, task),
305309
blockedBoundary,
@@ -308,7 +312,10 @@ function createTask(
308312
legacyContext,
309313
context,
310314
assignID,
311-
};
315+
}: any);
316+
if (__DEV__) {
317+
task.componentStack = currentTaskInDEV ? task.componentStack : null;
318+
}
312319
abortSet.add(task);
313320
return task;
314321
}
@@ -331,6 +338,55 @@ function createPendingSegment(
331338
};
332339
}
333340

341+
// DEV-only global reference to the currently executing task
342+
let currentTaskInDEV: null | Task = null;
343+
function getCurrentStackInDEV(): string {
344+
if (__DEV__) {
345+
if (currentTaskInDEV === null || currentTaskInDEV.componentStack === null) {
346+
return '';
347+
}
348+
return getStackByComponentStackNode(currentTaskInDEV.componentStack);
349+
}
350+
return '';
351+
}
352+
353+
function pushBuiltInComponentStackInDEV(task: Task, type: string): void {
354+
if (__DEV__) {
355+
task.componentStack = {
356+
tag: 0,
357+
parent: task.componentStack,
358+
type,
359+
};
360+
}
361+
}
362+
function pushFunctionComponentStackInDEV(task: Task, type: Function): void {
363+
if (__DEV__) {
364+
task.componentStack = {
365+
tag: 1,
366+
parent: task.componentStack,
367+
type,
368+
};
369+
}
370+
}
371+
function pushClassComponentStackInDEV(task: Task, type: Function): void {
372+
if (__DEV__) {
373+
task.componentStack = {
374+
tag: 2,
375+
parent: task.componentStack,
376+
type,
377+
};
378+
}
379+
}
380+
function popComponentStackInDEV(task: Task): void {
381+
if (__DEV__) {
382+
invariant(
383+
task.componentStack !== null,
384+
'Unexpectedly popped too many stack frames. This is a bug in React.',
385+
);
386+
task.componentStack = task.componentStack.parent;
387+
}
388+
}
389+
334390
function reportError(request: Request, error: mixed): void {
335391
// If this callback errors, we intentionally let that error bubble up to become a fatal error
336392
// so that someone fixes the error reporting instead of hiding it.
@@ -351,6 +407,7 @@ function renderSuspenseBoundary(
351407
task: Task,
352408
props: Object,
353409
): void {
410+
pushBuiltInComponentStackInDEV(task, 'Suspense');
354411
const parentBoundary = task.blockedBoundary;
355412
const parentSegment = task.blockedSegment;
356413

@@ -418,6 +475,7 @@ function renderSuspenseBoundary(
418475
} finally {
419476
task.blockedBoundary = parentBoundary;
420477
task.blockedSegment = parentSegment;
478+
popComponentStackInDEV(task);
421479
}
422480

423481
// This injects an extra segment just to contain an empty tag with an ID.
@@ -456,6 +514,7 @@ function renderHostElement(
456514
type: string,
457515
props: Object,
458516
): void {
517+
pushBuiltInComponentStackInDEV(task, type);
459518
const segment = task.blockedSegment;
460519
const children = pushStartInstance(
461520
segment.chunks,
@@ -476,6 +535,7 @@ function renderHostElement(
476535
// the correct context. Therefore this is not in a finally.
477536
segment.formatContext = prevContext;
478537
pushEndInstance(segment.chunks, type, props);
538+
popComponentStackInDEV(task);
479539
}
480540

481541
function shouldConstruct(Component) {
@@ -564,12 +624,14 @@ function renderClassComponent(
564624
Component: any,
565625
props: any,
566626
): void {
627+
pushClassComponentStackInDEV(task, Component);
567628
const maskedContext = !disableLegacyContext
568629
? getMaskedContext(Component, task.legacyContext)
569630
: undefined;
570631
const instance = constructClassInstance(Component, props, maskedContext);
571632
mountClassInstance(instance, Component, props, maskedContext);
572633
finishClassComponent(request, task, instance, Component, props);
634+
popComponentStackInDEV(task);
573635
}
574636

575637
const didWarnAboutBadClass = {};
@@ -594,6 +656,7 @@ function renderIndeterminateComponent(
594656
if (!disableLegacyContext) {
595657
legacyContext = getMaskedContext(Component, task.legacyContext);
596658
}
659+
pushFunctionComponentStackInDEV(task, Component);
597660

598661
if (__DEV__) {
599662
if (
@@ -688,6 +751,7 @@ function renderIndeterminateComponent(
688751
// the previous task every again, so we can use the destructive recursive form.
689752
renderNodeDestructive(request, task, value);
690753
}
754+
popComponentStackInDEV(task);
691755
}
692756

693757
function validateFunctionComponentInDev(Component: any): void {
@@ -768,8 +832,10 @@ function renderForwardRef(
768832
props: Object,
769833
ref: any,
770834
): void {
835+
pushFunctionComponentStackInDEV(task, type.render);
771836
const children = renderWithHooks(request, task, type.render, props, ref);
772837
renderNodeDestructive(request, task, children);
838+
popComponentStackInDEV(task);
773839
}
774840

775841
function renderMemo(
@@ -866,11 +932,13 @@ function renderLazyComponent(
866932
props: Object,
867933
ref: any,
868934
): void {
935+
pushBuiltInComponentStackInDEV(task, 'Lazy');
869936
const payload = lazyComponent._payload;
870937
const init = lazyComponent._init;
871938
const Component = init(payload);
872939
const resolvedProps = resolveDefaultProps(Component, props);
873-
return renderElement(request, task, Component, resolvedProps, ref);
940+
renderElement(request, task, Component, resolvedProps, ref);
941+
popComponentStackInDEV(task);
874942
}
875943

876944
function renderElement(
@@ -907,11 +975,17 @@ function renderElement(
907975
case REACT_DEBUG_TRACING_MODE_TYPE:
908976
case REACT_STRICT_MODE_TYPE:
909977
case REACT_PROFILER_TYPE:
910-
case REACT_SUSPENSE_LIST_TYPE: // TODO: SuspenseList should control the boundaries.
911978
case REACT_FRAGMENT_TYPE: {
912979
renderNodeDestructive(request, task, props.children);
913980
return;
914981
}
982+
case REACT_SUSPENSE_LIST_TYPE: {
983+
pushBuiltInComponentStackInDEV(task, 'SuspenseList');
984+
// TODO: SuspenseList should control the boundaries.
985+
renderNodeDestructive(request, task, props.children);
986+
popComponentStackInDEV(task);
987+
return;
988+
}
915989
case REACT_SCOPE_TYPE: {
916990
if (enableScopeAPI) {
917991
renderNodeDestructive(request, task, props.children);
@@ -1174,6 +1248,10 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
11741248
const previousFormatContext = task.blockedSegment.formatContext;
11751249
const previousLegacyContext = task.legacyContext;
11761250
const previousContext = task.context;
1251+
let previousComponentStack = null;
1252+
if (__DEV__) {
1253+
previousComponentStack = task.componentStack;
1254+
}
11771255
try {
11781256
return renderNodeDestructive(request, task, node);
11791257
} catch (x) {
@@ -1187,6 +1265,9 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
11871265
task.context = previousContext;
11881266
// Restore all active ReactContexts to what they were before.
11891267
switchContext(previousContext);
1268+
if (__DEV__) {
1269+
task.componentStack = previousComponentStack;
1270+
}
11901271
} else {
11911272
// Restore the context. We assume that this will be restored by the inner
11921273
// functions in case nothing throws so we don't use "finally" here.
@@ -1195,6 +1276,9 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
11951276
task.context = previousContext;
11961277
// Restore all active ReactContexts to what they were before.
11971278
switchContext(previousContext);
1279+
if (__DEV__) {
1280+
task.componentStack = previousComponentStack;
1281+
}
11981282
// We assume that we don't need the correct context.
11991283
// Let's terminate the rest of the tree and don't render any siblings.
12001284
throw x;
@@ -1360,6 +1444,11 @@ function retryTask(request: Request, task: Task): void {
13601444
// We don't restore it after we leave because it's likely that we'll end up
13611445
// needing a very similar context soon again.
13621446
switchContext(task.context);
1447+
let prevTaskInDEV = null;
1448+
if (__DEV__) {
1449+
prevTaskInDEV = currentTaskInDEV;
1450+
currentTaskInDEV = task;
1451+
}
13631452
try {
13641453
// We call the destructive form that mutates this task. That way if something
13651454
// suspends again, we can reuse the same task instead of spawning a new one.
@@ -1379,6 +1468,10 @@ function retryTask(request: Request, task: Task): void {
13791468
segment.status = ERRORED;
13801469
erroredTask(request, task.blockedBoundary, segment, x);
13811470
}
1471+
} finally {
1472+
if (__DEV__) {
1473+
currentTaskInDEV = prevTaskInDEV;
1474+
}
13821475
}
13831476
}
13841477

@@ -1389,6 +1482,11 @@ export function performWork(request: Request): void {
13891482
const prevContext = getActiveContext();
13901483
const prevDispatcher = ReactCurrentDispatcher.current;
13911484
ReactCurrentDispatcher.current = Dispatcher;
1485+
let prevGetCurrentStackImpl;
1486+
if (__DEV__) {
1487+
prevGetCurrentStackImpl = ReactDebugCurrentFrame.getCurrentStack;
1488+
ReactDebugCurrentFrame.getCurrentStack = getCurrentStackInDEV;
1489+
}
13921490
const prevResponseState = currentResponseState;
13931491
setCurrentResponseState(request.responseState);
13941492
try {
@@ -1408,6 +1506,9 @@ export function performWork(request: Request): void {
14081506
} finally {
14091507
setCurrentResponseState(prevResponseState);
14101508
ReactCurrentDispatcher.current = prevDispatcher;
1509+
if (__DEV__) {
1510+
ReactDebugCurrentFrame.getCurrentStack = prevGetCurrentStackImpl;
1511+
}
14111512
if (prevDispatcher === Dispatcher) {
14121513
// This means that we were in a reentrant work loop. This could happen
14131514
// in a renderer that supports synchronous work like renderToString,

0 commit comments

Comments
 (0)