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

[Fiber] Use Owner/JSX Stack When Appending Stacks to Console (#29206)

This one should be fully behind the `enableOwnerStacks` flag. Instead of printing the parent Component stack all the way to the root, this now prints the owner stack of every JSX callsite. It also includes intermediate callsites between the Component and the JSX call so it has potentially more frames. Mainly it provides the line number of the JSX callsite. In terms of the number of components is a subset of the parent component stack so it's less information in that regard. This is usually better since it's more focused on components that might affect the output but if it's contextual based on rendering it's still good to have parent stack. Therefore, I still use the parent stack when printing DOM nesting warnings but I plan on switching that format to a diff view format instead (Next.js already reformats the parent stack like this). __Follow ups__ - Server Components show up in the owner stack for client logs but logs done by Server Components don't yet get their owner stack printed as they're replayed. They're also not yet printed in the server logs of the RSC server. - Server Component stack frames are formatted as the server and added to the end but this might be a different format than the browser. E.g. if server is running V8 and browser is running JSC or vice versa. Ideally we can reformat them in terms of the client formatting. - This doesn't yet update Fizz or DevTools. Those will be follow ups. Fizz still prints parent stacks in the server side logs. The stacks added to user space `console.error` calls by DevTools still get the parent stacks instead. - It also doesn't yet expose these to user space so there's no way to get them inside `onCaughtError` for example or inside a custom `console.error` override. - In another follow up I'll use `console.createTask` instead and completely remove these stacks if it's available.

Sebastian Markbåge committed May 25, 2024 at 11:58 UTC d6cfa0f295f4c8b366af15fd20c84e27cdd1fab7
34 files changed +591 -145
.eslintrc.js
+1
@@ -486,6 +486,7 @@ module.exports = {
486 $ReadOnlyArray: 'readonly',
487 $ArrayBufferView: 'readonly',
488 $Shape: 'readonly',
489 + ConsoleTask: 'readonly', // TOOD: Figure out what the official name of this will be.
490 ReturnType: 'readonly',
491 AnimationFrameID: 'readonly',
492 // For Flow type annotation. Only `BigInt` is valid at runtime.
packages/react-client/src/__tests__/ReactFlight-test.js
+8 -8
@@ -1123,10 +1123,11 @@ describe('ReactFlight', () => {
1123 }
1124
1125 function App() {
1126 - return (
1127 - <Indirection>
1128 - <ClientComponent />
1129 - </Indirection>
1126 + // We use the ReactServer runtime here to get the Server owner.
1127 + return ReactServer.createElement(
1128 + Indirection,
1129 + null,
1130 + ReactServer.createElement(ClientComponent),
1131 );
1132 }
1133
@@ -1143,11 +1144,10 @@ describe('ReactFlight', () => {
1144 '\n' +
1145 'Check the render method of `Component`. See https://react.dev/link/warning-keys for more information.\n' +
1146 ' in span (at **)\n' +
1146 - // TODO: Because this validates after the div has been mounted, it is part of
1147 - // the parent stack but since owner stacks will switch to owners this goes away again.
1148 - (gate(flags => flags.enableOwnerStacks) ? ' in div (at **)\n' : '') +
1147 ' in Component (at **)\n' +
1150 - ' in Indirection (at **)\n' +
1148 + (gate(flags => flags.enableOwnerStacks)
1149 + ? ''
1150 + : ' in Indirection (at **)\n') +
1151 ' in App (at **)',
1152 );
1153 });
packages/react-dom-bindings/src/client/validateDOMNesting.js
+38 -12
@@ -7,6 +7,8 @@
7 * @flow
8 */
9
10 +import {getCurrentParentStackInDev} from 'react-reconciler/src/ReactCurrentFiber';
11 +
12 type Info = {tag: string};
13 export type AncestorInfoDev = {
14 current: ?Info,
@@ -476,19 +478,31 @@ function validateDOMNesting(
478 ' Add a <tbody>, <thead> or <tfoot> to your code to match the DOM tree generated by ' +
479 'the browser.';
480 }
479 - console.error(
480 - 'In HTML, %s cannot be a child of <%s>.%s\n' +
481 - 'This will cause a hydration error.',
481 + // Don't transform into consoleWithStackDev here because we add a manual stack.
482 + // We use the parent stack here instead of the owner stack because the parent
483 + // stack has more useful context for nesting.
484 + // TODO: Format this as a linkified "diff view" with props instead of
485 + // a stack trace since the stack trace format is now for owner stacks.
486 + console['error'](
487 + 'Warning: In HTML, %s cannot be a child of <%s>.%s\n' +
488 + 'This will cause a hydration error.%s',
489 tagDisplayName,
490 ancestorTag,
491 info,
492 + getCurrentParentStackInDev(),
493 );
494 } else {
487 - console.error(
488 - 'In HTML, %s cannot be a descendant of <%s>.\n' +
489 - 'This will cause a hydration error.',
495 + // Don't transform into consoleWithStackDev here because we add a manual stack.
496 + // We use the parent stack here instead of the owner stack because the parent
497 + // stack has more useful context for nesting.
498 + // TODO: Format this as a linkified "diff view" with props instead of
499 + // a stack trace since the stack trace format is now for owner stacks.
500 + console['error'](
501 + 'Warning: In HTML, %s cannot be a descendant of <%s>.\n' +
502 + 'This will cause a hydration error.%s',
503 tagDisplayName,
504 ancestorTag,
505 + getCurrentParentStackInDev(),
506 );
507 }
508 return false;
@@ -510,18 +524,30 @@ function validateTextNesting(childText: string, parentTag: string): boolean {
524 didWarn[warnKey] = true;
525
526 if (/\S/.test(childText)) {
513 - console.error(
514 - 'In HTML, text nodes cannot be a child of <%s>.\n' +
515 - 'This will cause a hydration error.',
527 + // Don't transform into consoleWithStackDev here because we add a manual stack.
528 + // We use the parent stack here instead of the owner stack because the parent
529 + // stack has more useful context for nesting.
530 + // TODO: Format this as a linkified "diff view" with props instead of
531 + // a stack trace since the stack trace format is now for owner stacks.
532 + console['error'](
533 + 'Warning: In HTML, text nodes cannot be a child of <%s>.\n' +
534 + 'This will cause a hydration error.%s',
535 parentTag,
536 + getCurrentParentStackInDev(),
537 );
538 } else {
519 - console.error(
520 - 'In HTML, whitespace text nodes cannot be a child of <%s>. ' +
539 + // Don't transform into consoleWithStackDev here because we add a manual stack.
540 + // We use the parent stack here instead of the owner stack because the parent
541 + // stack has more useful context for nesting.
542 + // TODO: Format this as a linkified "diff view" with props instead of
543 + // a stack trace since the stack trace format is now for owner stacks.
544 + console['error'](
545 + 'Warning: In HTML, whitespace text nodes cannot be a child of <%s>. ' +
546 "Make sure you don't have any extra whitespace between tags on " +
547 'each line of your source code.\n' +
523 - 'This will cause a hydration error.',
548 + 'This will cause a hydration error.%s',
549 parentTag,
550 + getCurrentParentStackInDev(),
551 );
552 }
553 return false;
packages/react-dom/src/__tests__/ReactChildReconciler-test.js
+6 -2
@@ -130,7 +130,9 @@ describe('ReactChildReconciler', () => {
130 'could change in a future version.\n' +
131 ' in div (at **)\n' +
132 ' in Component (at **)\n' +
133 - ' in Parent (at **)\n' +
133 + (gate(flags => flags.enableOwnerStacks)
134 + ? ''
135 + : ' in Parent (at **)\n') +
136 ' in GrandParent (at **)',
137 );
138 });
@@ -189,7 +191,9 @@ describe('ReactChildReconciler', () => {
191 'could change in a future version.\n' +
192 ' in div (at **)\n' +
193 ' in Component (at **)\n' +
192 - ' in Parent (at **)\n' +
194 + (gate(flags => flags.enableOwnerStacks)
195 + ? ''
196 + : ' in Parent (at **)\n') +
197 ' in GrandParent (at **)',
198 );
199 });
packages/react-dom/src/__tests__/ReactComponent-test.js
+6 -2
@@ -761,7 +761,9 @@ describe('ReactComponent', () => {
761 'Or maybe you meant to call this function rather than return it.\n' +
762 ' <span>{Foo}</span>\n' +
763 ' in span (at **)\n' +
764 - ' in div (at **)\n' +
764 + (gate(flags => flags.enableOwnerStacks)
765 + ? ''
766 + : ' in div (at **)\n') +
767 ' in Foo (at **)',
768 );
769 });
@@ -820,7 +822,9 @@ describe('ReactComponent', () => {
822 'Or maybe you meant to call this function rather than return it.\n' +
823 ' <span>{Foo}</span>\n' +
824 ' in span (at **)\n' +
823 - ' in div (at **)\n' +
825 + (gate(flags => flags.enableOwnerStacks)
826 + ? ''
827 + : ' in div (at **)\n') +
828 ' in Foo (at **)',
829 ]);
830 await act(() => {
packages/react-dom/src/__tests__/ReactDOM-test.js
+2 -2
@@ -552,7 +552,7 @@ describe('ReactDOM', () => {
552 // ReactDOM(App > div > span)
553 'Invalid ARIA attribute `ariaTypo`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
554 ' in span (at **)\n' +
555 - ' in div (at **)\n' +
555 + (gate(flags => flags.enableOwnerStacks) ? '' : ' in div (at **)\n') +
556 ' in App (at **)',
557 // ReactDOM(App > div > ServerEntry) >>> ReactDOMServer(Child) >>> ReactDOMServer(App2) >>> ReactDOMServer(blink)
558 'Invalid ARIA attribute `ariaTypo2`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
@@ -569,7 +569,7 @@ describe('ReactDOM', () => {
569 // ReactDOM(App > div > font)
570 'Invalid ARIA attribute `ariaTypo5`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
571 ' in font (at **)\n' +
572 - ' in div (at **)\n' +
572 + (gate(flags => flags.enableOwnerStacks) ? '' : ' in div (at **)\n') +
573 ' in App (at **)',
574 ]);
575 });
packages/react-dom/src/__tests__/ReactMultiChild-test.js
+6 -2
@@ -229,7 +229,9 @@ describe('ReactMultiChild', () => {
229 'could change in a future version.\n' +
230 ' in div (at **)\n' +
231 ' in WrapperComponent (at **)\n' +
232 - ' in div (at **)\n' +
232 + (gate(flags => flags.enableOwnerStacks)
233 + ? ''
234 + : ' in div (at **)\n') +
235 ' in Parent (at **)',
236 );
237 });
@@ -292,7 +294,9 @@ describe('ReactMultiChild', () => {
294 'could change in a future version.\n' +
295 ' in div (at **)\n' +
296 ' in WrapperComponent (at **)\n' +
295 - ' in div (at **)\n' +
297 + (gate(flags => flags.enableOwnerStacks)
298 + ? ''
299 + : ' in div (at **)\n') +
300 ' in Parent (at **)',
301 );
302 });
packages/react-dom/src/__tests__/ReactUpdates-test.js
+11 -2
@@ -1848,7 +1848,7 @@ describe('ReactUpdates', () => {
1848 it('warns about a deferred infinite update loop with useEffect', async () => {
1849 function NonTerminating() {
1850 const [step, setStep] = React.useState(0);
1851 - React.useEffect(() => {
1851 + React.useEffect(function myEffect() {
1852 setStep(x => x + 1);
1853 });
1854 return step;
@@ -1860,10 +1860,12 @@ describe('ReactUpdates', () => {
1860
1861 let error = null;
1862 let stack = null;
1863 + let nativeStack = null;
1864 const originalConsoleError = console.error;
1865 console.error = (e, s) => {
1866 error = e;
1867 stack = s;
1868 + nativeStack = new Error().stack;
1869 Scheduler.log('stop');
1870 };
1871 try {
@@ -1876,7 +1878,14 @@ describe('ReactUpdates', () => {
1878 }
1879
1880 expect(error).toContain('Maximum update depth exceeded');
1879 - expect(stack).toContain('at NonTerminating');
1881 + // The currently executing effect should be on the native stack
1882 + expect(nativeStack).toContain('at myEffect');
1883 + if (!gate(flags => flags.enableOwnerStacks)) {
1884 + // The currently running component's name is not in the owner
1885 + // stack because it's just its JSX callsite.
1886 + expect(stack).toContain('at NonTerminating');
1887 + }
1888 + expect(stack).toContain('at App');
1889 });
1890
1891 it('can have nested updates if they do not cross the limit', async () => {
packages/react-reconciler/src/ReactChildFiber.js
+44 -14
@@ -61,6 +61,7 @@ import {getIsHydrating} from './ReactFiberHydrationContext';
61 import {pushTreeFork} from './ReactFiberTreeContext';
62 import {createThenableState, trackUsedThenable} from './ReactFiberThenable';
63 import {readContextDuringReconciliation} from './ReactFiberNewContext';
64 +import {callLazyInitInDEV} from './ReactFiberCallUserSpace';
65
66 import {
67 getCurrentFiber as getCurrentDebugFiberInDEV,
@@ -362,6 +363,9 @@ function warnOnSymbolType(returnFiber: Fiber, invalidChild: symbol) {
363 }
364
365 function resolveLazy(lazyType: any) {
366 + if (__DEV__) {
367 + return callLazyInitInDEV(lazyType);
368 + }
369 const payload = lazyType._payload;
370 const init = lazyType._init;
371 return init(payload);
@@ -683,11 +687,17 @@ function createChildReconciler(
687 return created;
688 }
689 case REACT_LAZY_TYPE: {
686 - const payload = newChild._payload;
687 - const init = newChild._init;
690 + let resolvedChild;
691 + if (__DEV__) {
692 + resolvedChild = callLazyInitInDEV(newChild);
693 + } else {
694 + const payload = newChild._payload;
695 + const init = newChild._init;
696 + resolvedChild = init(payload);
697 + }
698 return createChild(
699 returnFiber,
690 - init(payload),
700 + resolvedChild,
701 lanes,
702 mergeDebugInfo(debugInfo, newChild._debugInfo), // call merge after init
703 );
@@ -811,12 +821,18 @@ function createChildReconciler(
821 }
822 }
823 case REACT_LAZY_TYPE: {
814 - const payload = newChild._payload;
815 - const init = newChild._init;
824 + let resolvedChild;
825 + if (__DEV__) {
826 + resolvedChild = callLazyInitInDEV(newChild);
827 + } else {
828 + const payload = newChild._payload;
829 + const init = newChild._init;
830 + resolvedChild = init(payload);
831 + }
832 return updateSlot(
833 returnFiber,
834 oldFiber,
819 - init(payload),
835 + resolvedChild,
836 lanes,
837 mergeDebugInfo(debugInfo, newChild._debugInfo),
838 );
@@ -937,17 +953,24 @@ function createChildReconciler(
953 debugInfo,
954 );
955 }
940 - case REACT_LAZY_TYPE:
941 - const payload = newChild._payload;
942 - const init = newChild._init;
956 + case REACT_LAZY_TYPE: {
957 + let resolvedChild;
958 + if (__DEV__) {
959 + resolvedChild = callLazyInitInDEV(newChild);
960 + } else {
961 + const payload = newChild._payload;
962 + const init = newChild._init;
963 + resolvedChild = init(payload);
964 + }
965 return updateFromMap(
966 existingChildren,
967 returnFiber,
968 newIdx,
947 - init(payload),
969 + resolvedChild,
970 lanes,
971 mergeDebugInfo(debugInfo, newChild._debugInfo),
972 );
973 + }
974 }
975
976 if (
@@ -1047,11 +1070,18 @@ function createChildReconciler(
1070 key,
1071 );
1072 break;
1050 - case REACT_LAZY_TYPE:
1051 - const payload = child._payload;
1052 - const init = (child._init: any);
1053 - warnOnInvalidKey(init(payload), knownKeys, returnFiber);
1073 + case REACT_LAZY_TYPE: {
1074 + let resolvedChild;
1075 + if (__DEV__) {
1076 + resolvedChild = callLazyInitInDEV((child: any));
1077 + } else {
1078 + const payload = child._payload;
1079 + const init = (child._init: any);
1080 + resolvedChild = init(payload);
1081 + }
1082 + warnOnInvalidKey(resolvedChild, knownKeys, returnFiber);
1083 break;
1084 + }
1085 default:
1086 break;
1087 }
packages/react-reconciler/src/ReactCurrentFiber.js
+21 -1
@@ -10,8 +10,12 @@
10 import type {Fiber} from './ReactInternalTypes';
11
12 import ReactSharedInternals from 'shared/ReactSharedInternals';
13 -import {getStackByFiberInDevAndProd} from './ReactFiberComponentStack';
13 +import {
14 + getStackByFiberInDevAndProd,
15 + getOwnerStackByFiberInDev,
16 +} from './ReactFiberComponentStack';
17 import {getComponentNameFromOwner} from 'react-reconciler/src/getComponentNameFromFiber';
18 +import {enableOwnerStacks} from 'shared/ReactFeatureFlags';
19
20 export let current: Fiber | null = null;
21 export let isRendering: boolean = false;
@@ -29,6 +33,17 @@ export function getCurrentFiberOwnerNameInDevOrNull(): string | null {
33 return null;
34 }
35
36 +export function getCurrentParentStackInDev(): string {
37 + // This is used to get the parent stack even with owner stacks turned on.
38 + if (__DEV__) {
39 + if (current === null) {
40 + return '';
41 + }
42 + return getStackByFiberInDevAndProd(current);
43 + }
44 + return '';
45 +}
46 +
47 function getCurrentFiberStackInDev(): string {
48 if (__DEV__) {
49 if (current === null) {
@@ -36,6 +51,11 @@ function getCurrentFiberStackInDev(): string {
51 }
52 // Safe because if current fiber exists, we are reconciling,
53 // and it is guaranteed to be the work-in-progress version.
54 + // TODO: The above comment is not actually true. We might be
55 + // in a commit phase or preemptive set state callback.
56 + if (enableOwnerStacks) {
57 + return getOwnerStackByFiberInDev(current);
58 + }
59 return getStackByFiberInDevAndProd(current);
60 }
61 return '';
packages/react-reconciler/src/ReactFiber.js
+13
@@ -38,6 +38,7 @@ import {
38 enableDO_NOT_USE_disableStrictPassiveEffect,
39 enableRenderableContext,
40 disableLegacyMode,
41 + enableOwnerStacks,
42 } from 'shared/ReactFeatureFlags';
43 import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
44 import {ConcurrentRoot} from './ReactRootTags';
@@ -205,6 +206,10 @@ function FiberNode(
206 // This isn't directly used but is handy for debugging internals:
207 this._debugInfo = null;
208 this._debugOwner = null;
209 + if (enableOwnerStacks) {
210 + this._debugStack = null;
211 + this._debugTask = null;
212 + }
213 this._debugNeedsRemount = false;
214 this._debugHookTypes = null;
215 if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') {
@@ -278,6 +283,10 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
283 // DEV-only fields
284
285 workInProgress._debugOwner = current._debugOwner;
286 + if (enableOwnerStacks) {
287 + workInProgress._debugStack = current._debugStack;
288 + workInProgress._debugTask = current._debugTask;
289 + }
290 workInProgress._debugHookTypes = current._debugHookTypes;
291 }
292
@@ -683,6 +692,10 @@ export function createFiberFromElement(
692 );
693 if (__DEV__) {
694 fiber._debugOwner = element._owner;
695 + if (enableOwnerStacks) {
696 + fiber._debugStack = element._debugStack;
697 + fiber._debugTask = element._debugTask;
698 + }
699 }
700 return fiber;
701 }
packages/react-reconciler/src/ReactFiberBeginWork.js
+29 -26
@@ -110,6 +110,7 @@ import {
110 disableLegacyMode,
111 disableDefaultPropsExceptForClasses,
112 disableStringRefs,
113 + enableOwnerStacks,
114 } from 'shared/ReactFeatureFlags';
115 import isArray from 'shared/isArray';
116 import shallowEqual from 'shared/shallowEqual';
@@ -124,7 +125,6 @@ import {
125 } from 'shared/ReactSymbols';
126 import {
127 getCurrentFiberOwnerNameInDevOrNull,
127 - setIsRendering,
128 setCurrentFiber,
129 } from './ReactCurrentFiber';
130 import {
@@ -297,6 +297,11 @@ import {
297 pushRootMarkerInstance,
298 TransitionTracingMarker,
299 } from './ReactFiberTracingMarkerComponent';
300 +import {
301 + callLazyInitInDEV,
302 + callComponentInDEV,
303 + callRenderInDEV,
304 +} from './ReactFiberCallUserSpace';
305
306 // A special exception that's used to unwind the stack when an update flows
307 // into a dehydrated boundary.
@@ -432,7 +437,6 @@ function updateForwardRef(
437 markComponentRenderStarted(workInProgress);
438 }
439 if (__DEV__) {
435 - setIsRendering(true);
440 nextChildren = renderWithHooks(
441 current,
442 workInProgress,
@@ -442,7 +446,6 @@ function updateForwardRef(
446 renderLanes,
447 );
448 hasId = checkDidRenderIdHook();
445 - setIsRendering(false);
449 } else {
450 nextChildren = renderWithHooks(
451 current,
@@ -1149,7 +1152,6 @@ function updateFunctionComponent(
1152 markComponentRenderStarted(workInProgress);
1153 }
1154 if (__DEV__) {
1152 - setIsRendering(true);
1155 nextChildren = renderWithHooks(
1156 current,
1157 workInProgress,
@@ -1159,7 +1161,6 @@ function updateFunctionComponent(
1161 renderLanes,
1162 );
1163 hasId = checkDidRenderIdHook();
1162 - setIsRendering(false);
1164 } else {
1165 nextChildren = renderWithHooks(
1166 current,
@@ -1393,20 +1394,18 @@ function finishClassComponent(
1394 markComponentRenderStarted(workInProgress);
1395 }
1396 if (__DEV__) {
1396 - setIsRendering(true);
1397 - nextChildren = instance.render();
1397 + nextChildren = callRenderInDEV(instance);
1398 if (
1399 debugRenderPhaseSideEffectsForStrictMode &&
1400 workInProgress.mode & StrictLegacyMode
1401 ) {
1402 setIsStrictModeForDevtools(true);
1403 try {
1404 - instance.render();
1404 + callRenderInDEV(instance);
1405 } finally {
1406 setIsStrictModeForDevtools(false);
1407 }
1408 }
1409 - setIsRendering(false);
1409 } else {
1410 nextChildren = instance.render();
1411 }
@@ -1766,9 +1765,14 @@ function mountLazyComponent(
1765
1766 const props = workInProgress.pendingProps;
1767 const lazyComponent: LazyComponentType<any, any> = elementType;
1769 - const payload = lazyComponent._payload;
1770 - const init = lazyComponent._init;
1771 - let Component = init(payload);
1768 + let Component;
1769 + if (__DEV__) {
1770 + Component = callLazyInitInDEV(lazyComponent);
1771 + } else {
1772 + const payload = lazyComponent._payload;
1773 + const init = lazyComponent._init;
1774 + Component = init(payload);
1775 + }
1776 // Store the unwrapped component in the type.
1777 workInProgress.type = Component;
1778
@@ -3417,9 +3421,7 @@ function updateContextConsumer(
3421 }
3422 let newChildren;
3423 if (__DEV__) {
3420 - setIsRendering(true);
3421 - newChildren = render(newValue);
3422 - setIsRendering(false);
3424 + newChildren = callComponentInDEV(render, newValue, undefined);
3425 } else {
3426 newChildren = render(newValue);
3427 }
@@ -3831,18 +3833,19 @@ function beginWork(
3833 if (__DEV__) {
3834 if (workInProgress._debugNeedsRemount && current !== null) {
3835 // This will restart the begin phase with a new fiber.
3834 - return remountFiber(
3835 - current,
3836 - workInProgress,
3837 - createFiberFromTypeAndProps(
3838 - workInProgress.type,
3839 - workInProgress.key,
3840 - workInProgress.pendingProps,
3841 - workInProgress._debugOwner || null,
3842 - workInProgress.mode,
3843 - workInProgress.lanes,
3844 - ),
3836 + const copiedFiber = createFiberFromTypeAndProps(
3837 + workInProgress.type,
3838 + workInProgress.key,
3839 + workInProgress.pendingProps,
3840 + workInProgress._debugOwner || null,
3841 + workInProgress.mode,
3842 + workInProgress.lanes,
3843 );
3844 + if (enableOwnerStacks) {
3845 + copiedFiber._debugStack = workInProgress._debugStack;
3846 + copiedFiber._debugTask = workInProgress._debugTask;
3847 + }
3848 + return remountFiber(current, workInProgress, copiedFiber);
3849 }
3850 }
3851
packages/react-reconciler/src/ReactFiberCallUserSpace.js new
+46
@@ -0,0 +1,46 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and 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 type {LazyComponent} from 'react/src/ReactLazy';
11 +
12 +import {setIsRendering} from './ReactCurrentFiber';
13 +
14 +// These indirections exists so we can exclude its stack frame in DEV (and anything below it).
15 +// TODO: Consider marking the whole bundle instead of these boundaries.
16 +
17 +/** @noinline */
18 +export function callComponentInDEV<Props, Arg, R>(
19 + Component: (p: Props, arg: Arg) => R,
20 + props: Props,
21 + secondArg: Arg,
22 +): R {
23 + setIsRendering(true);
24 + const result = Component(props, secondArg);
25 + setIsRendering(false);
26 + return result;
27 +}
28 +
29 +interface ClassInstance<R> {
30 + render(): R;
31 +}
32 +
33 +/** @noinline */
34 +export function callRenderInDEV<R>(instance: ClassInstance<R>): R {
35 + setIsRendering(true);
36 + const result = instance.render();
37 + setIsRendering(false);
38 + return result;
39 +}
40 +
41 +/** @noinline */
42 +export function callLazyInitInDEV(lazy: LazyComponent<any, any>): any {
43 + const payload = lazy._payload;
44 + const init = lazy._init;
45 + return init(payload);
46 +}
packages/react-reconciler/src/ReactFiberComponentStack.js
+105
@@ -7,7 +7,9 @@
7 * @flow
8 */
9
10 +import {enableOwnerStacks} from 'shared/ReactFeatureFlags';
11 import type {Fiber} from './ReactInternalTypes';
12 +import type {ReactComponentInfo} from 'shared/ReactTypes';
13
14 import {
15 HostComponent,
@@ -20,6 +22,7 @@ import {
22 ForwardRef,
23 SimpleMemoComponent,
24 ClassComponent,
25 + HostText,
26 } from './ReactWorkTags';
27 import {
28 describeBuiltInComponentFrame,
@@ -27,6 +30,7 @@ import {
30 describeClassComponentFrame,
31 describeDebugInfoFrame,
32 } from 'shared/ReactComponentStackFrame';
33 +import {formatOwnerStack} from './ReactFiberOwnerStack';
34
35 function describeFiber(fiber: Fiber): string {
36 switch (fiber.tag) {
@@ -78,3 +82,104 @@ export function getStackByFiberInDevAndProd(workInProgress: Fiber): string {
82 return '\nError generating stack: ' + x.message + '\n' + x.stack;
83 }
84 }
85 +
86 +function describeFunctionComponentFrameWithoutLineNumber(fn: Function): string {
87 + // We use this because we don't actually want to describe the line of the component
88 + // but just the component name.
89 + const name = fn ? fn.displayName || fn.name : '';
90 + return name ? describeBuiltInComponentFrame(name) : '';
91 +}
92 +
93 +export function getOwnerStackByFiberInDev(workInProgress: Fiber): string {
94 + if (!enableOwnerStacks || !__DEV__) {
95 + return '';
96 + }
97 + try {
98 + let info = '';
99 +
100 + if (workInProgress.tag === HostText) {
101 + // Text nodes never have an owner/stack because they're not created through JSX.
102 + // We use the parent since text nodes are always created through a host parent.
103 + workInProgress = (workInProgress.return: any);
104 + }
105 +
106 + // The owner stack of the current fiber will be where it was created, i.e. inside its owner.
107 + // There's no actual name of the currently executing component. Instead, that is available
108 + // on the regular stack that's currently executing. However, for built-ins there is no such
109 + // named stack frame and it would be ignored as being internal anyway. Therefore we add
110 + // add one extra frame just to describe the "current" built-in component by name.
111 + // Similarly, if there is no owner at all, then there's no stack frame so we add the name
112 + // of the root component to the stack to know which component is currently executing.
113 + switch (workInProgress.tag) {
114 + case HostHoistable:
115 + case HostSingleton:
116 + case HostComponent:
117 + info += describeBuiltInComponentFrame(workInProgress.type);
118 + break;
119 + case SuspenseComponent:
120 + info += describeBuiltInComponentFrame('Suspense');
121 + break;
122 + case SuspenseListComponent:
123 + info += describeBuiltInComponentFrame('SuspenseList');
124 + break;
125 + case FunctionComponent:
126 + case SimpleMemoComponent:
127 + case ClassComponent:
128 + if (!workInProgress._debugOwner) {
129 + info += describeFunctionComponentFrameWithoutLineNumber(
130 + workInProgress.type,
131 + );
132 + }
133 + break;
134 + case ForwardRef:
135 + if (!workInProgress._debugOwner) {
136 + info += describeFunctionComponentFrameWithoutLineNumber(
137 + workInProgress.type.render,
138 + );
139 + }
140 + break;
141 + }
142 +
143 + let owner: void | null | Fiber | ReactComponentInfo = workInProgress;
144 +
145 + while (owner) {
146 + if (typeof owner.tag === 'number') {
147 + const fiber: Fiber = (owner: any);
148 + owner = fiber._debugOwner;
149 + let debugStack = fiber._debugStack;
150 + // If we don't actually print the stack if there is no owner of this JSX element.
151 + // In a real app it's typically not useful since the root app is always controlled
152 + // by the framework. These also tend to have noisy stacks because they're not rooted
153 + // in a React render but in some imperative bootstrapping code. It could be useful
154 + // if the element was created in module scope. E.g. hoisted. We could add a a single
155 + // stack frame for context for example but it doesn't say much if that's a wrapper.
156 + if (owner && debugStack) {
157 + if (typeof debugStack !== 'string') {
158 + // Stash the formatted stack so that we can avoid redoing the filtering.
159 + fiber._debugStack = debugStack = formatOwnerStack(debugStack);
160 + }
161 + if (debugStack !== '') {
162 + info += '\n' + debugStack;
163 + }
164 + }
165 + } else if (typeof owner.stack === 'string') {
166 + // Server Component
167 + // The Server Component stack can come from a different VM that formats it different.
168 + // Likely V8. Since Chrome based browsers support createTask which is going to use
169 + // another code path anyway. I.e. this is likely NOT a V8 based browser.
170 + // This will cause some of the stack to have different formatting.
171 + // TODO: Normalize server component stacks to the client formatting.
172 + if (owner.stack !== '') {
173 + info += '\n' + owner.stack;
174 + }
175 + const componentInfo: ReactComponentInfo = (owner: any);
176 + owner = componentInfo.owner;
177 + } else {
178 + break;
179 + }
180 + }
181 + return info;
182 + } catch (x) {
183 + return '\nError generating stack: ' + x.message + '\n' + x.stack;
184 + }
185 +}
packages/react-reconciler/src/ReactFiberHooks.js
+8 -2
@@ -156,6 +156,8 @@ import {requestTransitionLane} from './ReactFiberRootScheduler';
156 import {isCurrentTreeHidden} from './ReactFiberHiddenContext';
157 import {requestCurrentTransition} from './ReactFiberTransition';
158
159 +import {callComponentInDEV} from './ReactFiberCallUserSpace';
160 +
161 export type Update<S, A> = {
162 lane: Lane,
163 revertLane: Lane,
@@ -587,7 +589,9 @@ export function renderWithHooks<Props, SecondArg>(
589 (workInProgress.mode & StrictLegacyMode) !== NoMode;
590
591 shouldDoubleInvokeUserFnsInHooksDEV = shouldDoubleRenderDEV;
590 - let children = Component(props, secondArg);
592 + let children = __DEV__
593 + ? callComponentInDEV(Component, props, secondArg)
594 + : Component(props, secondArg);
595 shouldDoubleInvokeUserFnsInHooksDEV = false;
596
597 // Check if there was a render phase update
@@ -819,7 +823,9 @@ function renderWithHooksAgain<Props, SecondArg>(
823 ? HooksDispatcherOnRerenderInDEV
824 : HooksDispatcherOnRerender;
825
822 - children = Component(props, secondArg);
826 + children = __DEV__
827 + ? callComponentInDEV(Component, props, secondArg)
828 + : Component(props, secondArg);
829 } while (didScheduleRenderPhaseUpdateDuringThisPass);
830 return children;
831 }
packages/react-reconciler/src/ReactFiberOwnerStack.js new
+112
@@ -0,0 +1,112 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and 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 {REACT_LAZY_TYPE} from 'shared/ReactSymbols';
11 +
12 +import {
13 + callLazyInitInDEV,
14 + callComponentInDEV,
15 + callRenderInDEV,
16 +} from './ReactFiberCallUserSpace';
17 +
18 +// TODO: Make this configurable on the root.
19 +const externalRegExp = /\/node\_modules\/|\(\<anonymous\>\)/;
20 +
21 +let callComponentFrame: null | string = null;
22 +let callIteratorFrame: null | string = null;
23 +let callLazyInitFrame: null | string = null;
24 +
25 +function isNotExternal(stackFrame: string): boolean {
26 + return !externalRegExp.test(stackFrame);
27 +}
28 +
29 +function initCallComponentFrame(): string {
30 + // Extract the stack frame of the callComponentInDEV function.
31 + const error = callComponentInDEV(Error, 'react-stack-top-frame', {});
32 + const stack = error.stack;
33 + const startIdx = stack.startsWith('Error: react-stack-top-frame\n') ? 29 : 0;
34 + const endIdx = stack.indexOf('\n', startIdx);
35 + if (endIdx === -1) {
36 + return stack.slice(startIdx);
37 + }
38 + return stack.slice(startIdx, endIdx);
39 +}
40 +
41 +function initCallRenderFrame(): string {
42 + // Extract the stack frame of the callRenderInDEV function.
43 + try {
44 + (callRenderInDEV: any)({render: null});
45 + return '';
46 + } catch (error) {
47 + const stack = error.stack;
48 + const startIdx = stack.startsWith('TypeError: ')
49 + ? stack.indexOf('\n') + 1
50 + : 0;
51 + const endIdx = stack.indexOf('\n', startIdx);
52 + if (endIdx === -1) {
53 + return stack.slice(startIdx);
54 + }
55 + return stack.slice(startIdx, endIdx);
56 + }
57 +}
58 +
59 +function initCallLazyInitFrame(): string {
60 + // Extract the stack frame of the callLazyInitInDEV function.
61 + const error = callLazyInitInDEV({
62 + $$typeof: REACT_LAZY_TYPE,
63 + _init: Error,
64 + _payload: 'react-stack-top-frame',
65 + });
66 + const stack = error.stack;
67 + const startIdx = stack.startsWith('Error: react-stack-top-frame\n') ? 29 : 0;
68 + const endIdx = stack.indexOf('\n', startIdx);
69 + if (endIdx === -1) {
70 + return stack.slice(startIdx);
71 + }
72 + return stack.slice(startIdx, endIdx);
73 +}
74 +
75 +function filterDebugStack(error: Error): string {
76 + // Since stacks can be quite large and we pass a lot of them, we filter them out eagerly
77 + // to save bandwidth even in DEV. We'll also replay these stacks on the client so by
78 + // stripping them early we avoid that overhead. Otherwise we'd normally just rely on
79 + // the DevTools or framework's ignore lists to filter them out.
80 + let stack = error.stack;
81 + if (stack.startsWith('Error: react-stack-top-frame\n')) {
82 + // V8's default formatting prefixes with the error message which we
83 + // don't want/need.
84 + stack = stack.slice(29);
85 + }
86 + const frames = stack.split('\n').slice(1);
87 + if (callComponentFrame === null) {
88 + callComponentFrame = initCallComponentFrame();
89 + }
90 + let lastFrameIdx = frames.indexOf(callComponentFrame);
91 + if (lastFrameIdx === -1) {
92 + if (callLazyInitFrame === null) {
93 + callLazyInitFrame = initCallLazyInitFrame();
94 + }
95 + lastFrameIdx = frames.indexOf(callLazyInitFrame);
96 + if (lastFrameIdx === -1) {
97 + if (callIteratorFrame === null) {
98 + callIteratorFrame = initCallRenderFrame();
99 + }
100 + lastFrameIdx = frames.indexOf(callIteratorFrame);
101 + }
102 + }
103 + if (lastFrameIdx !== -1) {
104 + // Cut off everything after our "callComponent" slot since it'll be Fiber internals.
105 + frames.length = lastFrameIdx;
106 + }
107 + return frames.filter(isNotExternal).join('\n');
108 +}
109 +
110 +export function formatOwnerStack(ownerStackTrace: Error): string {
111 + return filterDebugStack(ownerStackTrace);
112 +}
packages/react-reconciler/src/ReactInternalTypes.js
+2
@@ -195,6 +195,8 @@ export type Fiber = {
195
196 _debugInfo?: ReactDebugInfo | null,
197 _debugOwner?: ReactComponentInfo | Fiber | null,
198 + _debugStack?: string | Error | null,
199 + _debugTask?: ConsoleTask | null,
200 _debugIsCurrentlyTiming?: boolean,
201 _debugNeedsRemount?: boolean,
202
packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
+2
@@ -716,6 +716,7 @@ describe('ReactHooks', () => {
716 useImperativeHandle(ref, () => {}, props.deps);
717 return null;
718 });
719 + App.displayName = 'App';
720
721 await expect(async () => {
722 await act(() => {
@@ -846,6 +847,7 @@ describe('ReactHooks', () => {
847 });
848 return null;
849 });
850 + App.displayName = 'App';
851
852 await expect(async () => {
853 await act(() => {
packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js
+17 -13
@@ -93,9 +93,11 @@ describe('ReactIncrementalErrorLogging', () => {
93 ),
94 expect.stringMatching(
95 new RegExp(
96 - '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
97 - '\\s+(in|at) span(.*)\n' +
98 - '\\s+(in|at) div(.*)',
96 + gate(flags => flags.enableOwnerStacks)
97 + ? '\\s+(in|at) ErrorThrowingComponent'
98 + : '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
99 + '\\s+(in|at) span(.*)\n' +
100 + '\\s+(in|at) div(.*)',
101 ),
102 ),
103 );
@@ -139,9 +141,11 @@ describe('ReactIncrementalErrorLogging', () => {
141 ),
142 expect.stringMatching(
143 new RegExp(
142 - '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
143 - '\\s+(in|at) span(.*)\n' +
144 - '\\s+(in|at) div(.*)',
144 + gate(flags => flags.enableOwnerStacks)
145 + ? '\\s+(in|at) ErrorThrowingComponent'
146 + : '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
147 + '\\s+(in|at) span(.*)\n' +
148 + '\\s+(in|at) div(.*)',
149 ),
150 ),
151 );
@@ -197,10 +201,12 @@ describe('ReactIncrementalErrorLogging', () => {
201 ),
202 expect.stringMatching(
203 new RegExp(
200 - '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
201 - '\\s+(in|at) span(.*)\n' +
202 - '\\s+(in|at) ErrorBoundary(.*)\n' +
203 - '\\s+(in|at) div(.*)',
204 + gate(flags => flags.enableOwnerStacks)
205 + ? '\\s+(in|at) ErrorThrowingComponent'
206 + : '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
207 + '\\s+(in|at) span(.*)\n' +
208 + '\\s+(in|at) ErrorBoundary(.*)\n' +
209 + '\\s+(in|at) div(.*)',
210 ),
211 ),
212 );
@@ -278,9 +284,7 @@ describe('ReactIncrementalErrorLogging', () => {
284 ),
285 expect.stringMatching(
286 gate(flag => flag.enableOwnerStacks)
281 - ? // With owner stacks the return path is cut off but in this case
282 - // this is also what the owner stack looks like.
283 - new RegExp('\\s+(in|at) Foo (.*)')
287 + ? new RegExp('\\s+(in|at) Foo')
288 : new RegExp(
289 '\\s+(in|at) Foo (.*)\n' + '\\s+(in|at) ErrorBoundary(.*)',
290 ),
packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
+13 -8
@@ -228,10 +228,18 @@ describe('ReactLazy', () => {
228
229 expect(error.message).toMatch('Element type is invalid');
230 assertLog(['Loading...']);
231 - assertConsoleErrorDev([
232 - 'Expected the result of a dynamic import() call',
233 - 'Expected the result of a dynamic import() call',
234 - ]);
231 + assertConsoleErrorDev(
232 + [
233 + 'Expected the result of a dynamic import() call',
234 + 'Expected the result of a dynamic import() call',
235 + ],
236 + gate(flags => flags.enableOwnerStacks)
237 + ? {
238 + // There's no owner
239 + withoutStack: true,
240 + }
241 + : undefined,
242 + );
243 expect(root).not.toMatchRenderedOutput('Hi');
244 });
245
@@ -996,10 +1004,7 @@ describe('ReactLazy', () => {
1004 await act(() => resolveFakeImport(Foo));
1005 assertLog(['A', 'B']);
1006 }).toErrorDev(
999 - ' in Text (at **)\n' +
1000 - // TODO: Because this validates after the div has been mounted, it is part of
1001 - // the parent stack but since owner stacks will switch to owners this goes away again.
1002 - (gate(flags => flags.enableOwnerStacks) ? ' in div (at **)\n' : '') +
1007 + (gate(flags => flags.enableOwnerStacks) ? '' : ' in Text (at **)\n') +
1008 ' in Foo (at **)',
1009 );
1010 expect(root).toMatchRenderedOutput(<div>AB</div>);
packages/react-reconciler/src/__tests__/ReactMemo-test.js
+12 -12
@@ -602,7 +602,7 @@ describe('memo', () => {
602 'Each child in a list should have a unique "key" prop. ' +
603 'See https://react.dev/link/warning-keys for more information.\n' +
604 ' in span (at **)\n' +
605 - ' in p (at **)',
605 + ' in ',
606 );
607 });
608
@@ -622,16 +622,16 @@ describe('memo', () => {
622 '\n\nCheck the top-level render call using <Inner>. It was passed a child from Inner. ' +
623 'See https://react.dev/link/warning-keys for more information.\n' +
624 ' in span (at **)\n' +
625 - ' in Inner (at **)\n' +
626 - ' in p (at **)',
625 + ' in Inner (at **)' +
626 + (gate(flags => flags.enableOwnerStacks) ? '' : '\n in p (at **)'),
627 );
628 });
629
630 - it('should use the inner displayName in the stack', async () => {
630 + it('should use the inner name in the stack', async () => {
631 const fn = (props, ref) => {
632 return [<span />];
633 };
634 - fn.displayName = 'Inner';
634 + Object.defineProperty(fn, 'name', {value: 'Inner'});
635 const MemoComponent = React.memo(fn);
636 ReactNoop.render(
637 <p>
@@ -645,8 +645,8 @@ describe('memo', () => {
645 '\n\nCheck the top-level render call using <Inner>. It was passed a child from Inner. ' +
646 'See https://react.dev/link/warning-keys for more information.\n' +
647 ' in span (at **)\n' +
648 - ' in Inner (at **)\n' +
649 - ' in p (at **)',
648 + ' in Inner (at **)' +
649 + (gate(flags => flags.enableOwnerStacks) ? '' : '\n in p (at **)'),
650 );
651 });
652
@@ -667,8 +667,8 @@ describe('memo', () => {
667 '\n\nCheck the top-level render call using <Outer>. It was passed a child from Outer. ' +
668 'See https://react.dev/link/warning-keys for more information.\n' +
669 ' in span (at **)\n' +
670 - ' in Outer (at **)\n' +
671 - ' in p (at **)',
670 + ' in Outer (at **)' +
671 + (gate(flags => flags.enableOwnerStacks) ? '' : '\n in p (at **)'),
672 );
673 });
674
@@ -676,7 +676,7 @@ describe('memo', () => {
676 const fn = (props, ref) => {
677 return [<span />];
678 };
679 - fn.displayName = 'Inner';
679 + Object.defineProperty(fn, 'name', {value: 'Inner'});
680 const MemoComponent = React.memo(fn);
681 MemoComponent.displayName = 'Outer';
682 ReactNoop.render(
@@ -691,8 +691,8 @@ describe('memo', () => {
691 '\n\nCheck the top-level render call using <Inner>. It was passed a child from Inner. ' +
692 'See https://react.dev/link/warning-keys for more information.\n' +
693 ' in span (at **)\n' +
694 - ' in Inner (at **)\n' +
695 - ' in p (at **)',
694 + ' in Inner (at **)' +
695 + (gate(flags => flags.enableOwnerStacks) ? '' : '\n in p (at **)'),
696 );
697 });
698 }
packages/react-server/src/ReactFizzServer.js
+1
@@ -752,6 +752,7 @@ function getCurrentStackInDEV(): string {
752 if (currentTaskInDEV === null || currentTaskInDEV.componentStack === null) {
753 return '';
754 }
755 + // TODO: Support owner based stacks for logs during SSR.
756 return getStackByComponentStackNode(currentTaskInDEV.componentStack);
757 }
758 return '';
packages/react/src/ReactForwardRef.js
+3 -1
@@ -68,7 +68,9 @@ export function forwardRef<Props, ElementType: React$ElementType>(
68 // React.forwardRef((props, ref) => {...});
69 // This kind of inner function is not used elsewhere so the side effect is okay.
70 if (!render.name && !render.displayName) {
71 - render.displayName = name;
71 + Object.defineProperty(render, 'name', {
72 + value: name,
73 + });
74 }
75 },
76 });
packages/react/src/ReactMemo.js
+3 -1
@@ -48,7 +48,9 @@ export function memo<Props>(
48 // React.memo((props) => {...});
49 // This kind of inner function is not used elsewhere so the side effect is okay.
50 if (!type.name && !type.displayName) {
51 - type.displayName = name;
51 + Object.defineProperty(type, 'name', {
52 + value: name,
53 + });
54 }
55 },
56 });
packages/react/src/__tests__/ReactCreateRef-test.js
+8 -4
@@ -45,8 +45,10 @@ describe('ReactCreateRef', () => {
45 ).toErrorDev(
46 'Unexpected ref object provided for div. ' +
47 'Use either a ref-setter function or React.createRef().\n' +
48 - ' in div (at **)\n' +
49 - ' in Wrapper (at **)',
48 + ' in div (at **)' +
49 + (gate(flags => flags.enableOwnerStacks)
50 + ? ''
51 + : '\n in Wrapper (at **)'),
52 );
53
54 expect(() =>
@@ -60,8 +62,10 @@ describe('ReactCreateRef', () => {
62 ).toErrorDev(
63 'Unexpected ref object provided for ExampleComponent. ' +
64 'Use either a ref-setter function or React.createRef().\n' +
63 - ' in ExampleComponent (at **)\n' +
64 - ' in Wrapper (at **)',
65 + ' in ExampleComponent (at **)' +
66 + (gate(flags => flags.enableOwnerStacks)
67 + ? ''
68 + : '\n in Wrapper (at **)'),
69 );
70 });
71 });
packages/react/src/__tests__/ReactElementValidator-test.internal.js
+3 -6
@@ -142,11 +142,10 @@ describe('ReactElementValidator', () => {
142 '"key" prop.\n\nCheck the render method of `Component`. See ' +
143 'https://react.dev/link/warning-keys for more information.\n' +
144 ' in div (at **)\n' +
145 - // TODO: Because this validates after the div has been mounted, it is part of
146 - // the parent stack but since owner stacks will switch to owners this goes away again.
147 - (gate(flags => flags.enableOwnerStacks) ? ' in div (at **)\n' : '') +
145 ' in Component (at **)\n' +
149 - ' in Parent (at **)\n' +
146 + (gate(flags => flags.enableOwnerStacks)
147 + ? ''
148 + : ' in Parent (at **)\n') +
149 ' in GrandParent (at **)',
150 );
151 });
@@ -262,8 +261,6 @@ describe('ReactElementValidator', () => {
261 'Each child in a list should have a unique "key" prop.' +
262 '\n\nCheck the render method of `ParentComp`. It was passed a child from MyComp. ' +
263 'See https://react.dev/link/warning-keys for more information.\n' +
265 - // TODO: Because this validates after the div has been mounted, it is part of
266 - // the parent stack but since owner stacks will switch to owners this goes away again.
264 ' in div (at **)\n' +
265 ' in MyComp (at **)\n' +
266 ' in ParentComp (at **)',
packages/react/src/__tests__/ReactJSXElementValidator-test.js
-2
@@ -209,8 +209,6 @@ describe('ReactJSXElementValidator', () => {
209 'Each child in a list should have a unique "key" prop.' +
210 '\n\nCheck the render method of `ParentComp`. It was passed a child from MyComp. ' +
211 'See https://react.dev/link/warning-keys for more information.\n' +
212 - // TODO: Because this validates after the div has been mounted, it is part of
213 - // the parent stack but since owner stacks will switch to owners this goes away again.
212 ' in div (at **)\n' +
213 ' in MyComp (at **)\n' +
214 ' in ParentComp (at **)',
packages/react/src/__tests__/ReactJSXRuntime-test.js
+3 -4
@@ -299,10 +299,9 @@ describe('ReactJSXRuntime', () => {
299 }).toErrorDev(
300 'Warning: Each child in a list should have a unique "key" prop.\n\n' +
301 'Check the render method of `Parent`. See https://react.dev/link/warning-keys for more information.\n' +
302 - ' in Child (at **)\n' +
303 - // TODO: Because this validates after the div has been mounted, it is part of
304 - // the parent stack but since owner stacks will switch to owners this goes away again.
305 - (gate(flags => flags.enableOwnerStacks) ? ' in div (at **)\n' : '') +
302 + (gate(flags => flags.enableOwnerStacks)
303 + ? ''
304 + : ' in Child (at **)\n') +
305 ' in Parent (at **)',
306 );
307 });
packages/react/src/__tests__/createReactClassIntegration-test.js
+2
@@ -594,6 +594,7 @@ describe('create-react-class-integration', () => {
594 return null;
595 },
596 });
597 + Component.displayName = 'Component';
598
599 await expect(async () => {
600 await expect(async () => {
@@ -643,6 +644,7 @@ describe('create-react-class-integration', () => {
644 return null;
645 },
646 });
647 + Component.displayName = 'Component';
648
649 await expect(async () => {
650 await expect(async () => {
packages/react/src/__tests__/forwardRef-test.js
+13 -13
@@ -197,7 +197,7 @@ describe('forwardRef', () => {
197 '\n\nCheck the top-level render call using <ForwardRef>. It was passed a child from ForwardRef. ' +
198 'See https://react.dev/link/warning-keys for more information.\n' +
199 ' in span (at **)\n' +
200 - ' in p (at **)',
200 + ' in ',
201 );
202 });
203
@@ -217,16 +217,16 @@ describe('forwardRef', () => {
217 '\n\nCheck the top-level render call using <ForwardRef(Inner)>. It was passed a child from ForwardRef(Inner). ' +
218 'See https://react.dev/link/warning-keys for more information.\n' +
219 ' in span (at **)\n' +
220 - ' in Inner (at **)\n' +
221 - ' in p (at **)',
220 + ' in Inner (at **)' +
221 + (gate(flags => flags.enableOwnerStacks) ? '' : '\n in p (at **)'),
222 );
223 });
224
225 - it('should use the inner displayName in the stack', async () => {
225 + it('should use the inner name in the stack', async () => {
226 const fn = (props, ref) => {
227 return [<span />];
228 };
229 - fn.displayName = 'Inner';
229 + Object.defineProperty(fn, 'name', {value: 'Inner'});
230 const RefForwardingComponent = React.forwardRef(fn);
231 ReactNoop.render(
232 <p>
@@ -240,8 +240,8 @@ describe('forwardRef', () => {
240 '\n\nCheck the top-level render call using <ForwardRef(Inner)>. It was passed a child from ForwardRef(Inner). ' +
241 'See https://react.dev/link/warning-keys for more information.\n' +
242 ' in span (at **)\n' +
243 - ' in Inner (at **)\n' +
244 - ' in p (at **)',
243 + ' in Inner (at **)' +
244 + (gate(flags => flags.enableOwnerStacks) ? '' : '\n in p (at **)'),
245 );
246 });
247
@@ -262,16 +262,16 @@ describe('forwardRef', () => {
262 '\n\nCheck the top-level render call using <Outer>. It was passed a child from Outer. ' +
263 'See https://react.dev/link/warning-keys for more information.\n' +
264 ' in span (at **)\n' +
265 - ' in Outer (at **)\n' +
266 - ' in p (at **)',
265 + ' in Outer (at **)' +
266 + (gate(flags => flags.enableOwnerStacks) ? '' : '\n in p (at **)'),
267 );
268 });
269
270 - it('should prefer the inner to the outer displayName in the stack', async () => {
270 + it('should prefer the inner name to the outer displayName in the stack', async () => {
271 const fn = (props, ref) => {
272 return [<span />];
273 };
274 - fn.displayName = 'Inner';
274 + Object.defineProperty(fn, 'name', {value: 'Inner'});
275 const RefForwardingComponent = React.forwardRef(fn);
276 RefForwardingComponent.displayName = 'Outer';
277 ReactNoop.render(
@@ -286,8 +286,8 @@ describe('forwardRef', () => {
286 '\n\nCheck the top-level render call using <Outer>. It was passed a child from Outer. ' +
287 'See https://react.dev/link/warning-keys for more information.\n' +
288 ' in span (at **)\n' +
289 - ' in Inner (at **)\n' +
290 - ' in p (at **)',
289 + ' in Inner (at **)' +
290 + (gate(flags => flags.enableOwnerStacks) ? '' : '\n in p (at **)'),
291 );
292 });
293
packages/react/src/jsx/ReactJSXElement.js
+44 -4
@@ -492,7 +492,16 @@ export function jsxProdSignatureRunningInDevWithDynamicChildren(
492 ) {
493 if (__DEV__) {
494 const isStaticChildren = false;
495 - return jsxDEV(type, config, maybeKey, isStaticChildren, source, self);
495 + return jsxDEVImpl(
496 + type,
497 + config,
498 + maybeKey,
499 + isStaticChildren,
500 + source,
501 + self,
502 + __DEV__ && enableOwnerStacks ? Error('react-stack-top-frame') : undefined,
503 + __DEV__ && enableOwnerStacks ? createTask(getTaskName(type)) : undefined,
504 + );
505 }
506 }
507
@@ -505,7 +514,16 @@ export function jsxProdSignatureRunningInDevWithStaticChildren(
514 ) {
515 if (__DEV__) {
516 const isStaticChildren = true;
508 - return jsxDEV(type, config, maybeKey, isStaticChildren, source, self);
517 + return jsxDEVImpl(
518 + type,
519 + config,
520 + maybeKey,
521 + isStaticChildren,
522 + source,
523 + self,
524 + __DEV__ && enableOwnerStacks ? Error('react-stack-top-frame') : undefined,
525 + __DEV__ && enableOwnerStacks ? createTask(getTaskName(type)) : undefined,
526 + );
527 }
528 }
529
@@ -518,6 +536,28 @@ const didWarnAboutKeySpread = {};
536 * @param {string} key
537 */
538 export function jsxDEV(type, config, maybeKey, isStaticChildren, source, self) {
539 + return jsxDEVImpl(
540 + type,
541 + config,
542 + maybeKey,
543 + isStaticChildren,
544 + source,
545 + self,
546 + __DEV__ && enableOwnerStacks ? Error('react-stack-top-frame') : undefined,
547 + __DEV__ && enableOwnerStacks ? createTask(getTaskName(type)) : undefined,
548 + );
549 +}
550 +
551 +function jsxDEVImpl(
552 + type,
553 + config,
554 + maybeKey,
555 + isStaticChildren,
556 + source,
557 + self,
558 + debugStack,
559 + debugTask,
560 +) {
561 if (__DEV__) {
562 if (!isValidElementType(type)) {
563 // This is an invalid element type.
@@ -716,8 +756,8 @@ export function jsxDEV(type, config, maybeKey, isStaticChildren, source, self) {
756 source,
757 getOwner(),
758 props,
719 - __DEV__ && enableOwnerStacks ? Error('react-stack-top-frame') : undefined,
720 - __DEV__ && enableOwnerStacks ? createTask(getTaskName(type)) : undefined,
759 + debugStack,
760 + debugTask,
761 );
762 }
763 }
packages/shared/ReactElementType.js
-4
@@ -9,10 +9,6 @@
9
10 import type {ReactDebugInfo} from './ReactTypes';
11
12 -interface ConsoleTask {
13 - run<T>(f: () => T): T;
14 -}
15 -
12 export type ReactElement = {
13 $$typeof: any,
14 type: any,
scripts/flow/environment.js
+4
@@ -29,6 +29,10 @@ declare module 'create-react-class' {
29 declare const exports: React$CreateClass;
30 }
31
32 +declare interface ConsoleTask {
33 + run<T>(f: () => T): T;
34 +}
35 +
36 // Flow hides the props of React$Element, this overrides it to unhide
37 // them for React internals.
38 // prettier-ignore
scripts/jest/matchers/toWarnDev.js
+5
@@ -16,6 +16,11 @@ function normalizeCodeLocInfo(str) {
16 // React format:
17 // in Component (at filename.js:123)
18 return str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) {
19 + if (name.endsWith('.render')) {
20 + // Class components will have the `render` method as part of their stack trace.
21 + // We strip that out in our normalization to make it look more like component stacks.
22 + name = name.slice(0, name.length - 7);
23 + }
24 return '\n in ' + name + ' (at **)';
25 });
26 }