Skip to content

Commit

Permalink
[Fizz] Implement Component Stacks in DEV for warnings (#21610)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
sebmarkbage authored Jun 3, 2021
1 parent 39f0074 commit 1b7b359
Show file tree
Hide file tree
Showing 3 changed files with 292 additions and 4 deletions.
112 changes: 112 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,118 @@ describe('ReactDOMFizzServer', () => {
);
});

function normalizeCodeLocInfo(str) {
return (
str &&
str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
return '\n in ' + name + ' (at **)';
})
);
}

// @gate experimental
it('should include a component stack across suspended boundaries', async () => {
function B() {
const children = [readText('Hello'), readText('World')];
// Intentionally trigger a key warning here.
return (
<div>
{children.map(t => (
<span>{t}</span>
))}
</div>
);
}
function C() {
return (
<inCorrectTag>
<Text text="Loading" />
</inCorrectTag>
);
}
function A() {
return (
<div>
<Suspense fallback={<C />}>
<B />
</Suspense>
</div>
);
}

// We can't use the toErrorDev helper here because this is an async act.
const originalConsoleError = console.error;
const mockError = jest.fn();
console.error = (...args) => {
mockError(...args.map(normalizeCodeLocInfo));
};

try {
await act(async () => {
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
<A />,
writable,
);
startWriting();
});

expect(getVisibleChildren(container)).toEqual(
<div>
<incorrecttag>Loading</incorrecttag>
</div>,
);

if (__DEV__) {
expect(mockError).toHaveBeenCalledWith(
'Warning: <%s /> is using incorrect casing. Use PascalCase for React components, or lowercase for HTML elements.%s',
'inCorrectTag',
'\n' +
' in inCorrectTag (at **)\n' +
' in C (at **)\n' +
' in Suspense (at **)\n' +
' in div (at **)\n' +
' in A (at **)',
);
mockError.mockClear();
} else {
expect(mockError).not.toHaveBeenCalled();
}

await act(async () => {
resolveText('Hello');
resolveText('World');
});

if (__DEV__) {
expect(mockError).toHaveBeenCalledWith(
'Warning: Each child in a list should have a unique "key" prop.%s%s' +
' See https://reactjs.org/link/warning-keys for more information.%s',
'\n\nCheck the top-level render call using <div>.',
'',
'\n' +
' in span (at **)\n' +
' in B (at **)\n' +
' in Suspense (at **)\n' +
' in div (at **)\n' +
' in A (at **)',
);
} else {
expect(mockError).not.toHaveBeenCalled();
}

expect(getVisibleChildren(container)).toEqual(
<div>
<div>
<span>Hello</span>
<span>World</span>
</div>
</div>,
);
} finally {
console.error = originalConsoleError;
}
});

// @gate experimental
it('should can suspend in a class component with legacy context', async () => {
class TestProvider extends React.Component {
Expand Down
61 changes: 61 additions & 0 deletions packages/react-server/src/ReactFizzComponentStack.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import {
describeBuiltInComponentFrame,
describeFunctionComponentFrame,
describeClassComponentFrame,
} from 'shared/ReactComponentStackFrame';

// DEV-only reverse linked list representing the current component stack
type BuiltInComponentStackNode = {
tag: 0,
parent: null | ComponentStackNode,
type: string,
};
type FunctionComponentStackNode = {
tag: 1,
parent: null | ComponentStackNode,
type: Function,
};
type ClassComponentStackNode = {
tag: 2,
parent: null | ComponentStackNode,
type: Function,
};
export type ComponentStackNode =
| BuiltInComponentStackNode
| FunctionComponentStackNode
| ClassComponentStackNode;

export function getStackByComponentStackNode(
componentStack: ComponentStackNode,
): string {
try {
let info = '';
let node = componentStack;
do {
switch (node.tag) {
case 0:
info += describeBuiltInComponentFrame(node.type, null, null);
break;
case 1:
info += describeFunctionComponentFrame(node.type, null, null);
break;
case 2:
info += describeClassComponentFrame(node.type, null, null);
break;
}
node = node.parent;
} while (node);
return info;
} catch (x) {
return '\nError generating stack: ' + x.message + '\n' + x.stack;
}
}
Loading

0 comments on commit 1b7b359

Please sign in to comment.