Skip to content

Commit 1b7b359

Browse files
authored
[Fizz] Implement Component Stacks in DEV for warnings (#21610)
* Implement component stacks This uses a reverse linked list in DEV-only to keep track of where we're currently executing. * Fix bug that wasn't picking up the right stack at suspended boundaries This makes it more explicit which stack we pass in to be retained by the task.
1 parent 39f0074 commit 1b7b359

File tree

3 files changed

+292
-4
lines changed

3 files changed

+292
-4
lines changed

packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,6 +1058,118 @@ describe('ReactDOMFizzServer', () => {
10581058
);
10591059
});
10601060

1061+
function normalizeCodeLocInfo(str) {
1062+
return (
1063+
str &&
1064+
str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
1065+
return '\n in ' + name + ' (at **)';
1066+
})
1067+
);
1068+
}
1069+
1070+
// @gate experimental
1071+
it('should include a component stack across suspended boundaries', async () => {
1072+
function B() {
1073+
const children = [readText('Hello'), readText('World')];
1074+
// Intentionally trigger a key warning here.
1075+
return (
1076+
<div>
1077+
{children.map(t => (
1078+
<span>{t}</span>
1079+
))}
1080+
</div>
1081+
);
1082+
}
1083+
function C() {
1084+
return (
1085+
<inCorrectTag>
1086+
<Text text="Loading" />
1087+
</inCorrectTag>
1088+
);
1089+
}
1090+
function A() {
1091+
return (
1092+
<div>
1093+
<Suspense fallback={<C />}>
1094+
<B />
1095+
</Suspense>
1096+
</div>
1097+
);
1098+
}
1099+
1100+
// We can't use the toErrorDev helper here because this is an async act.
1101+
const originalConsoleError = console.error;
1102+
const mockError = jest.fn();
1103+
console.error = (...args) => {
1104+
mockError(...args.map(normalizeCodeLocInfo));
1105+
};
1106+
1107+
try {
1108+
await act(async () => {
1109+
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
1110+
<A />,
1111+
writable,
1112+
);
1113+
startWriting();
1114+
});
1115+
1116+
expect(getVisibleChildren(container)).toEqual(
1117+
<div>
1118+
<incorrecttag>Loading</incorrecttag>
1119+
</div>,
1120+
);
1121+
1122+
if (__DEV__) {
1123+
expect(mockError).toHaveBeenCalledWith(
1124+
'Warning: <%s /> is using incorrect casing. Use PascalCase for React components, or lowercase for HTML elements.%s',
1125+
'inCorrectTag',
1126+
'\n' +
1127+
' in inCorrectTag (at **)\n' +
1128+
' in C (at **)\n' +
1129+
' in Suspense (at **)\n' +
1130+
' in div (at **)\n' +
1131+
' in A (at **)',
1132+
);
1133+
mockError.mockClear();
1134+
} else {
1135+
expect(mockError).not.toHaveBeenCalled();
1136+
}
1137+
1138+
await act(async () => {
1139+
resolveText('Hello');
1140+
resolveText('World');
1141+
});
1142+
1143+
if (__DEV__) {
1144+
expect(mockError).toHaveBeenCalledWith(
1145+
'Warning: Each child in a list should have a unique "key" prop.%s%s' +
1146+
' See https://reactjs.org/link/warning-keys for more information.%s',
1147+
'\n\nCheck the top-level render call using <div>.',
1148+
'',
1149+
'\n' +
1150+
' in span (at **)\n' +
1151+
' in B (at **)\n' +
1152+
' in Suspense (at **)\n' +
1153+
' in div (at **)\n' +
1154+
' in A (at **)',
1155+
);
1156+
} else {
1157+
expect(mockError).not.toHaveBeenCalled();
1158+
}
1159+
1160+
expect(getVisibleChildren(container)).toEqual(
1161+
<div>
1162+
<div>
1163+
<span>Hello</span>
1164+
<span>World</span>
1165+
</div>
1166+
</div>,
1167+
);
1168+
} finally {
1169+
console.error = originalConsoleError;
1170+
}
1171+
});
1172+
10611173
// @gate experimental
10621174
it('should can suspend in a class component with legacy context', async () => {
10631175
class TestProvider extends React.Component {
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+
}

0 commit comments

Comments
 (0)