@samitouri / QOS-React-1 / commits / 84239da896

Move createElement/JSX Warnings into the Renderer (#29088)

This is necessary to simplify the component stack handling to make way for owner stacks. It also solves some hacks that we used to have but don't quite make sense. It also solves the problem where things like key warnings get silenced in RSC because they get deduped. It also surfaces areas where we were missing key warnings to begin with. Almost every type of warning is issued from the renderer. React Elements are really not anything special themselves. They're just lazily invoked functions and its really the renderer that determines there semantics. We have three types of warnings that previously fired in JSX/createElement: - Fragment props validation. - Type validation. - Key warning. It's nice to be able to do some validation in the JSX/createElement because it has a more specific stack frame at the callsite. However, that's the case for every type of component and validation. That's the whole point of enableOwnerStacks. It's also not sufficient to do it in JSX/createElement so we also have validation in the renderers too. So this validation is really just an eager validation but also happens again later. The problem with these is that we don't really know what types are valid until we get to the renderer. Additionally, by placing it in the isomorphic code it becomes harder to do deduping of warnings in a way that makes sense for that renderer. It also means we can't reuse logic for managing stacks etc. Fragment props validation really should just be part of the renderer like any other component type. This also matters once we add Fragment refs and other fragment features. So I moved this into Fiber. However, since some Fragments don't have Fibers, I do the validation in ChildFiber instead of beginWork where it would normally happen. For `type` validation we already do validation when rendering. By leaving it to the renderer we don't have to hard code an extra list. This list also varies by context. E.g. class components aren't allowed in RSC but client references are but we don't have an isomorphic way to identify client references because they're defined by the host config so the current logic is flawed anyway. I kept the early validation for now without the `enableOwnerStacks` since it does provide a nicer stack frame but with that flag on it'll be handled with nice stacks anyway. I normalized some of the errors to ensure tests pass. For `key` validation it's the same principle. The mechanism for the heuristic is still the same - if it passes statically through a parent JSX/createElement call then it's considered validated. We already did print the error later from the renderer so this also disables the early log in the `enableOwnerStacks` flag. I also added logging to Fizz so that key warnings can print in SSR logs. Flight is a bit more complex. For elements that end up on the client we just pass the `validated` flag along to the client and let the client renderer print the error once rendered. For server components we log the error from Flight with the server component as the owner on the stack which will allow us to print the right stack for context. The factoring of this is a little tricky because we only want to warn if it's in an array parent but we want to log the error later to get the right debug info. Fiber/Fizz has a similar factoring problem that causes us to create a fake Fiber for the owner which means the logs won't be associated with the right place in DevTools.

Sebastian Markbåge committed May 23, 2024 at 12:48 UTC 84239da896fd7395a667ab1e7ef1ef338a32de8f
31 files changed +874 -384
fixtures/flight/server/region.js
+6 -3
@@ -81,17 +81,20 @@ async function renderApp(res, returnValue, formState) {
81 ).main.css;
82 }
83 const App = m.default.default || m.default;
84 - const root = [
84 + const root = React.createElement(
85 + React.Fragment,
86 + null,
87 // Prepend the App's tree with stylesheets required for this entrypoint.
88 mainCSSChunks.map(filename =>
89 React.createElement('link', {
90 rel: 'stylesheet',
91 href: filename,
92 precedence: 'default',
93 + key: filename,
94 })
95 ),
93 - React.createElement(App),
94 - ];
96 + React.createElement(App)
97 + );
98 // For client-invoked server actions we refresh the tree and return a return value.
99 const payload = {root, returnValue, formState};
100 const {pipe} = renderToPipeableStream(payload, moduleMap);
packages/react-client/src/ReactFlightClient.js
+5 -3
@@ -579,6 +579,7 @@ function createElement(
579 props: mixed,
580 owner: null | ReactComponentInfo, // DEV-only
581 stack: null | string, // DEV-only
582 + validated: number, // DEV-only
583 ): React$Element<any> {
584 let element: any;
585 if (__DEV__ && enableRefAsProp) {
@@ -624,13 +625,13 @@ function createElement(
625 // Unfortunately, _store is enumerable in jest matchers so for equality to
626 // work, I need to keep it or make _store non-enumerable in the other file.
627 element._store = ({}: {
627 - validated?: boolean,
628 + validated?: number,
629 });
630 Object.defineProperty(element._store, 'validated', {
631 configurable: false,
632 enumerable: false,
633 writable: true,
633 - value: true, // This element has already been validated on the server.
634 + value: enableOwnerStacks ? validated : 1, // Whether the element has already been validated on the server.
635 });
636 // debugInfo contains Server Component debug information.
637 Object.defineProperty(element, '_debugInfo', {
@@ -644,7 +645,7 @@ function createElement(
645 configurable: false,
646 enumerable: false,
647 writable: true,
647 - value: {stack: stack},
648 + value: stack,
649 });
650 Object.defineProperty(element, '_debugTask', {
651 configurable: false,
@@ -1053,6 +1054,7 @@ function parseModelTuple(
1054 tuple[3],
1055 __DEV__ ? (tuple: any)[4] : null,
1056 __DEV__ && enableOwnerStacks ? (tuple: any)[5] : null,
1057 + __DEV__ && enableOwnerStacks ? (tuple: any)[6] : 0,
1058 );
1059 }
1060 return value;
packages/react-client/src/__tests__/ReactFlight-test.js
+32 -8
@@ -1143,6 +1143,9 @@ describe('ReactFlight', () => {
1143 '\n' +
1144 'Check the render method of `Component`. See https://react.dev/link/warning-keys for more information.\n' +
1145 ' 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' : '') +
1149 ' in Component (at **)\n' +
1150 ' in Indirection (at **)\n' +
1151 ' in App (at **)',
@@ -1386,19 +1389,40 @@ describe('ReactFlight', () => {
1389 ReactNoopFlightClient.read(transport);
1390 });
1391
1389 - it('should warn in DEV a child is missing keys', () => {
1392 + it('should warn in DEV a child is missing keys on server component', () => {
1393 + function NoKey({children}) {
1394 + return <div key="this has a key but parent doesn't" />;
1395 + }
1396 + expect(() => {
1397 + const transport = ReactNoopFlightServer.render(
1398 + <div>{Array(6).fill(<NoKey />)}</div>,
1399 + );
1400 + ReactNoopFlightClient.read(transport);
1401 + }).toErrorDev('Each child in a list should have a unique "key" prop.', {
1402 + withoutStack: gate(flags => flags.enableOwnerStacks),
1403 + });
1404 + });
1405 +
1406 + it('should warn in DEV a child is missing keys in client component', async () => {
1407 function ParentClient({children}) {
1408 return children;
1409 }
1410 const Parent = clientReference(ParentClient);
1394 - expect(() => {
1411 + await expect(async () => {
1412 const transport = ReactNoopFlightServer.render(
1413 <Parent>{Array(6).fill(<div>no key</div>)}</Parent>,
1414 );
1415 ReactNoopFlightClient.read(transport);
1416 + await act(async () => {
1417 + ReactNoop.render(await ReactNoopFlightClient.read(transport));
1418 + });
1419 }).toErrorDev(
1400 - 'Each child in a list should have a unique "key" prop. ' +
1401 - 'See https://react.dev/link/warning-keys for more information.',
1420 + gate(flags => flags.enableOwnerStacks)
1421 + ? 'Each child in a list should have a unique "key" prop.' +
1422 + '\n\nCheck the top-level render call using <ParentClient>. ' +
1423 + 'See https://react.dev/link/warning-keys for more information.'
1424 + : 'Each child in a list should have a unique "key" prop. ' +
1425 + 'See https://react.dev/link/warning-keys for more information.',
1426 );
1427 });
1428
@@ -2306,7 +2330,7 @@ describe('ReactFlight', () => {
2330 }
2331
2332 function ThirdPartyFragmentComponent() {
2309 - return [<span>Who</span>, ' ', <span>dis?</span>];
2333 + return [<span key="1">Who</span>, ' ', <span key="2">dis?</span>];
2334 }
2335
2336 function ServerComponent({transport}) {
@@ -2318,7 +2342,7 @@ describe('ReactFlight', () => {
2342 const promiseComponent = Promise.resolve(<ThirdPartyComponent />);
2343
2344 const thirdPartyTransport = ReactNoopFlightServer.render(
2321 - [promiseComponent, lazy, <ThirdPartyFragmentComponent />],
2345 + [promiseComponent, lazy, <ThirdPartyFragmentComponent key="3" />],
2346 {
2347 environmentName: 'third-party',
2348 },
@@ -2410,8 +2434,8 @@ describe('ReactFlight', () => {
2434 const iteratorPromise = new Promise(r => (resolve = r));
2435
2436 async function* ThirdPartyAsyncIterableComponent({item, initial}) {
2413 - yield <span>Who</span>;
2414 - yield <span>dis?</span>;
2437 + yield <span key="1">Who</span>;
2438 + yield <span key="2">dis?</span>;
2439 resolve();
2440 }
2441
packages/react-devtools-shared/src/__tests__/inspectedElement-test.js
+1 -7
@@ -2562,13 +2562,7 @@ describe('InspectedElement', () => {
2562 const data = await getErrorsAndWarningsForElementAtIndex(0);
2563 expect(data).toMatchInlineSnapshot(`
2564 {
2565 - "errors": [
2566 - [
2567 - "Warning: Each child in a list should have a unique "key" prop. See https://react.dev/link/warning-keys for more information.
2568 - at Example",
2569 - 1,
2570 - ],
2571 - ],
2565 + "errors": [],
2566 "warnings": [],
2567 }
2568 `);
packages/react-devtools-shared/src/__tests__/store-test.js
+1 -2
@@ -1932,9 +1932,8 @@ describe('Store', () => {
1932 );
1933
1934 expect(store).toMatchInlineSnapshot(`
1935 - ✕ 1, ⚠ 0
1935 [root]
1937 - ▾ <Example> ✕
1936 + ▾ <Example>
1937 <Child>
1938 `);
1939 });
packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js
-80
@@ -12,22 +12,8 @@
12 // while still maintaining support for multiple renderer versions
13 // (which use different values for ReactTypeOfWork).
14
15 -import type {LazyComponent} from 'react/src/ReactLazy';
15 import type {CurrentDispatcherRef} from './types';
16
18 -import {
19 - FORWARD_REF_NUMBER,
20 - FORWARD_REF_SYMBOL_STRING,
21 - LAZY_NUMBER,
22 - LAZY_SYMBOL_STRING,
23 - MEMO_NUMBER,
24 - MEMO_SYMBOL_STRING,
25 - SUSPENSE_NUMBER,
26 - SUSPENSE_SYMBOL_STRING,
27 - SUSPENSE_LIST_NUMBER,
28 - SUSPENSE_LIST_SYMBOL_STRING,
29 -} from './ReactSymbols';
30 -
17 // The shared console patching code is DEV-only.
18 // We can't use it since DevTools only ships production builds.
19 import {disableLogs, reenableLogs} from './DevToolsConsolePatching';
@@ -297,69 +283,3 @@ export function describeFunctionComponentFrame(
283 ): string {
284 return describeNativeComponentFrame(fn, false, currentDispatcherRef);
285 }
300 -
301 -function shouldConstruct(Component: Function) {
302 - const prototype = Component.prototype;
303 - return !!(prototype && prototype.isReactComponent);
304 -}
305 -
306 -export function describeUnknownElementTypeFrameInDEV(
307 - type: any,
308 - currentDispatcherRef: CurrentDispatcherRef,
309 -): string {
310 - if (!__DEV__) {
311 - return '';
312 - }
313 - if (type == null) {
314 - return '';
315 - }
316 - if (typeof type === 'function') {
317 - return describeNativeComponentFrame(
318 - type,
319 - shouldConstruct(type),
320 - currentDispatcherRef,
321 - );
322 - }
323 - if (typeof type === 'string') {
324 - return describeBuiltInComponentFrame(type);
325 - }
326 - switch (type) {
327 - case SUSPENSE_NUMBER:
328 - case SUSPENSE_SYMBOL_STRING:
329 - return describeBuiltInComponentFrame('Suspense');
330 - case SUSPENSE_LIST_NUMBER:
331 - case SUSPENSE_LIST_SYMBOL_STRING:
332 - return describeBuiltInComponentFrame('SuspenseList');
333 - }
334 - if (typeof type === 'object') {
335 - switch (type.$$typeof) {
336 - case FORWARD_REF_NUMBER:
337 - case FORWARD_REF_SYMBOL_STRING:
338 - return describeFunctionComponentFrame(
339 - type.render,
340 - currentDispatcherRef,
341 - );
342 - case MEMO_NUMBER:
343 - case MEMO_SYMBOL_STRING:
344 - // Memo may contain any component type so we recursively resolve it.
345 - return describeUnknownElementTypeFrameInDEV(
346 - type.type,
347 - currentDispatcherRef,
348 - );
349 - case LAZY_NUMBER:
350 - case LAZY_SYMBOL_STRING: {
351 - const lazyComponent: LazyComponent<any, any> = (type: any);
352 - const payload = lazyComponent._payload;
353 - const init = lazyComponent._init;
354 - try {
355 - // Lazy may contain any component type so we recursively resolve it.
356 - return describeUnknownElementTypeFrameInDEV(
357 - init(payload),
358 - currentDispatcherRef,
359 - );
360 - } catch (x) {}
361 - }
362 - }
363 - }
364 - return '';
365 -}
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+22 -3
@@ -1833,10 +1833,18 @@ describe('ReactDOMFizzServer', () => {
1833 expect(mockError).toHaveBeenCalledWith(
1834 'Warning: Each child in a list should have a unique "key" prop.%s%s' +
1835 ' See https://react.dev/link/warning-keys for more information.%s',
1836 - '\n\nCheck the top-level render call using <div>.',
1836 + gate(flags => flags.enableOwnerStacks)
1837 + ? // We currently don't track owners in Fizz which is responsible for this frame.
1838 + ''
1839 + : '\n\nCheck the top-level render call using <div>.',
1840 '',
1841 '\n' +
1842 ' in span (at **)\n' +
1843 + // TODO: Because this validates after the div has been mounted, it is part of
1844 + // the parent stack but since owner stacks will switch to owners this goes away again.
1845 + (gate(flags => flags.enableOwnerStacks)
1846 + ? ' in div (at **)\n'
1847 + : '') +
1848 ' in B (at **)\n' +
1849 ' in Suspense (at **)\n' +
1850 ' in div (at **)\n' +
@@ -1890,7 +1898,12 @@ describe('ReactDOMFizzServer', () => {
1898 </b>
1899 );
1900 if (this.props.prefix) {
1893 - return [readText(this.props.prefix), child];
1901 + return (
1902 + <>
1903 + {readText(this.props.prefix)}
1904 + {child}
1905 + </>
1906 + );
1907 }
1908 return child;
1909 }
@@ -1900,7 +1913,13 @@ describe('ReactDOMFizzServer', () => {
1913 const {pipe} = renderToPipeableStream(
1914 <TestProvider ctx="A">
1915 <div>
1903 - <Suspense fallback={[<Text text="Loading: " />, <TestConsumer />]}>
1916 + <Suspense
1917 + fallback={
1918 + <>
1919 + <Text text="Loading: " />
1920 + <TestConsumer />
1921 + </>
1922 + }>
1923 <TestProvider ctx="B">
1924 <TestConsumer prefix="Hello: " />
1925 </TestProvider>
packages/react-dom/src/__tests__/ReactServerRendering-test.js
+1 -1
@@ -819,7 +819,7 @@ describe('ReactDOMServer', () => {
819 }
820
821 function Child() {
822 - return [<A key="1" />, <B key="2" />, <span ariaTypo2="no" />];
822 + return [<A key="1" />, <B key="2" />, <span ariaTypo2="no" key="3" />];
823 }
824
825 function App() {
packages/react-reconciler/src/ReactChildFiber.js
+115 -9
@@ -62,6 +62,11 @@ import {pushTreeFork} from './ReactFiberTreeContext';
62 import {createThenableState, trackUsedThenable} from './ReactFiberThenable';
63 import {readContextDuringReconciliation} from './ReactFiberNewContext';
64
65 +import {
66 + getCurrentFiber as getCurrentDebugFiberInDEV,
67 + setCurrentFiber as setCurrentDebugFiberInDEV,
68 +} from './ReactCurrentFiber';
69 +
70 // This tracks the thenables that are unwrapped during reconcilation.
71 let thenableState: ThenableState | null = null;
72 let thenableIndexCounter: number = 0;
@@ -109,7 +114,11 @@ if (__DEV__) {
114 if (child === null || typeof child !== 'object') {
115 return;
116 }
112 - if (!child._store || child._store.validated || child.key != null) {
117 + if (
118 + !child._store ||
119 + ((child._store.validated || child.key != null) &&
120 + child._store.validated !== 2)
121 + ) {
122 return;
123 }
124
@@ -121,23 +130,115 @@ if (__DEV__) {
130 }
131
132 // $FlowFixMe[cannot-write] unable to narrow type from mixed to writable object
124 - child._store.validated = true;
133 + child._store.validated = 1;
134
126 - const componentName = getComponentNameFromFiber(returnFiber) || 'Component';
135 + const componentName = getComponentNameFromFiber(returnFiber);
136
128 - if (ownerHasKeyUseWarning[componentName]) {
137 + const componentKey = componentName || 'null';
138 + if (ownerHasKeyUseWarning[componentKey]) {
139 return;
140 }
131 - ownerHasKeyUseWarning[componentName] = true;
141 + ownerHasKeyUseWarning[componentKey] = true;
142 +
143 + const childOwner = child._owner;
144 + const parentOwner = returnFiber._debugOwner;
145 +
146 + let currentComponentErrorInfo = '';
147 + if (parentOwner && typeof parentOwner.tag === 'number') {
148 + const name = getComponentNameFromFiber((parentOwner: any));
149 + if (name) {
150 + currentComponentErrorInfo =
151 + '\n\nCheck the render method of `' + name + '`.';
152 + }
153 + }
154 + if (!currentComponentErrorInfo) {
155 + if (componentName) {
156 + currentComponentErrorInfo = `\n\nCheck the top-level render call using <${componentName}>.`;
157 + }
158 + }
159 +
160 + // Usually the current owner is the offender, but if it accepts children as a
161 + // property, it may be the creator of the child that's responsible for
162 + // assigning it a key.
163 + let childOwnerAppendix = '';
164 + if (childOwner != null && parentOwner !== childOwner) {
165 + let ownerName = null;
166 + if (typeof childOwner.tag === 'number') {
167 + ownerName = getComponentNameFromFiber((childOwner: any));
168 + } else if (typeof childOwner.name === 'string') {
169 + ownerName = childOwner.name;
170 + }
171 + if (ownerName) {
172 + // Give the component that originally created this child.
173 + childOwnerAppendix = ` It was passed a child from ${ownerName}.`;
174 + }
175 + }
176 +
177 + // We create a fake Fiber for the child to log the stack trace from.
178 + // TODO: Refactor the warnForMissingKey calls to happen after fiber creation
179 + // so that we can get access to the fiber that will eventually be created.
180 + // That way the log can show up associated with the right instance in DevTools.
181 + const fiber = createFiberFromElement((child: any), returnFiber.mode, 0);
182 + fiber.return = returnFiber;
183
184 + const prevDebugFiber = getCurrentDebugFiberInDEV();
185 + setCurrentDebugFiberInDEV(fiber);
186 console.error(
134 - 'Each child in a list should have a unique ' +
135 - '"key" prop. See https://react.dev/link/warning-keys for ' +
136 - 'more information.',
187 + 'Each child in a list should have a unique "key" prop.' +
188 + '%s%s See https://react.dev/link/warning-keys for more information.',
189 + currentComponentErrorInfo,
190 + childOwnerAppendix,
191 );
192 + setCurrentDebugFiberInDEV(prevDebugFiber);
193 };
194 }
195
196 +// Given a fragment, validate that it can only be provided with fragment props
197 +// We do this here instead of BeginWork because the Fragment fiber doesn't have
198 +// the whole props object, only the children and is shared with arrays.
199 +function validateFragmentProps(
200 + element: ReactElement,
201 + fiber: null | Fiber,
202 + returnFiber: Fiber,
203 +) {
204 + if (__DEV__) {
205 + const keys = Object.keys(element.props);
206 + for (let i = 0; i < keys.length; i++) {
207 + const key = keys[i];
208 + if (key !== 'children' && key !== 'key') {
209 + if (fiber === null) {
210 + // For unkeyed root fragments there's no Fiber. We create a fake one just for
211 + // error stack handling.
212 + fiber = createFiberFromElement(element, returnFiber.mode, 0);
213 + fiber.return = returnFiber;
214 + }
215 + const prevDebugFiber = getCurrentDebugFiberInDEV();
216 + setCurrentDebugFiberInDEV(fiber);
217 + console.error(
218 + 'Invalid prop `%s` supplied to `React.Fragment`. ' +
219 + 'React.Fragment can only have `key` and `children` props.',
220 + key,
221 + );
222 + setCurrentDebugFiberInDEV(prevDebugFiber);
223 + break;
224 + }
225 + }
226 +
227 + if (!enableRefAsProp && element.ref !== null) {
228 + if (fiber === null) {
229 + // For unkeyed root fragments there's no Fiber. We create a fake one just for
230 + // error stack handling.
231 + fiber = createFiberFromElement(element, returnFiber.mode, 0);
232 + fiber.return = returnFiber;
233 + }
234 + const prevDebugFiber = getCurrentDebugFiberInDEV();
235 + setCurrentDebugFiberInDEV(fiber);
236 + console.error('Invalid attribute `ref` supplied to `React.Fragment`.');
237 + setCurrentDebugFiberInDEV(prevDebugFiber);
238 + }
239 + }
240 +}
241 +
242 function unwrapThenable<T>(thenable: Thenable<T>): T {
243 const index = thenableIndexCounter;
244 thenableIndexCounter += 1;
@@ -416,7 +517,7 @@ function createChildReconciler(
517 ): Fiber {
518 const elementType = element.type;
519 if (elementType === REACT_FRAGMENT_TYPE) {
419 - return updateFragment(
520 + const updated = updateFragment(
521 returnFiber,
522 current,
523 element.props.children,
@@ -424,6 +525,8 @@ function createChildReconciler(
525 element.key,
526 debugInfo,
527 );
528 + validateFragmentProps(element, updated, returnFiber);
529 + return updated;
530 }
531 if (current !== null) {
532 if (
@@ -1481,6 +1584,7 @@ function createChildReconciler(
1584 existing._debugOwner = element._owner;
1585 existing._debugInfo = debugInfo;
1586 }
1587 + validateFragmentProps(element, existing, returnFiber);
1588 return existing;
1589 }
1590 } else {
@@ -1530,6 +1634,7 @@ function createChildReconciler(
1634 if (__DEV__) {
1635 created._debugInfo = debugInfo;
1636 }
1637 + validateFragmentProps(element, created, returnFiber);
1638 return created;
1639 } else {
1640 const created = createFiberFromElement(element, returnFiber.mode, lanes);
@@ -1607,6 +1712,7 @@ function createChildReconciler(
1712 newChild.type === REACT_FRAGMENT_TYPE &&
1713 newChild.key === null;
1714 if (isUnkeyedTopLevelFragment) {
1715 + validateFragmentProps(newChild, null, returnFiber);
1716 newChild = newChild.props.children;
1717 }
1718
packages/react-reconciler/src/ReactFiber.js
+26 -3
@@ -103,6 +103,7 @@ import {
103 REACT_OFFSCREEN_TYPE,
104 REACT_LEGACY_HIDDEN_TYPE,
105 REACT_TRACING_MARKER_TYPE,
106 + REACT_ELEMENT_TYPE,
107 } from 'shared/ReactSymbols';
108 import {TransitionTracingMarker} from './ReactFiberTracingMarkerComponent';
109 import {
@@ -111,6 +112,8 @@ import {
112 } from './ReactFiberCommitWork';
113 import {getHostContext} from './ReactFiberHostContext';
114 import type {ReactComponentInfo} from '../../shared/ReactTypes';
115 +import isArray from 'shared/isArray';
116 +import getComponentNameFromType from 'shared/getComponentNameFromType';
117
118 export type {Fiber};
119
@@ -599,6 +602,7 @@ export function createFiberFromTypeAndProps(
602 }
603 }
604 let info = '';
605 + let typeString;
606 if (__DEV__) {
607 if (
608 type === undefined ||
@@ -608,19 +612,38 @@ export function createFiberFromTypeAndProps(
612 ) {
613 info +=
614 ' You likely forgot to export your component from the file ' +
611 - "it's defined in, or you might have mixed up default and " +
612 - 'named imports.';
615 + "it's defined in, or you might have mixed up default and named imports.";
616 }
617 +
618 + if (type === null) {
619 + typeString = 'null';
620 + } else if (isArray(type)) {
621 + typeString = 'array';
622 + } else if (
623 + type !== undefined &&
624 + type.$$typeof === REACT_ELEMENT_TYPE
625 + ) {
626 + typeString = `<${
627 + getComponentNameFromType(type.type) || 'Unknown'
628 + } />`;
629 + info =
630 + ' Did you accidentally export a JSX literal instead of a component?';
631 + } else {
632 + typeString = typeof type;
633 + }
634 +
635 const ownerName = owner ? getComponentNameFromOwner(owner) : null;
636 if (ownerName) {
637 info += '\n\nCheck the render method of `' + ownerName + '`.';
638 }
639 + } else {
640 + typeString = type === null ? 'null' : typeof type;
641 }
642
643 throw new Error(
644 'Element type is invalid: expected a string (for built-in ' +
645 'components) or a class/function (for composite components) ' +
623 - `but got: ${type == null ? type : typeof type}.${info}`,
646 + `but got: ${typeString}.${info}`,
647 );
648 }
649 }
packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
+7 -1
@@ -995,7 +995,13 @@ describe('ReactLazy', () => {
995 await expect(async () => {
996 await act(() => resolveFakeImport(Foo));
997 assertLog(['A', 'B']);
998 - }).toErrorDev(' in Text (at **)\n' + ' in Foo (at **)');
998 + }).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' : '') +
1003 + ' in Foo (at **)',
1004 + );
1005 expect(root).toMatchRenderedOutput(<div>AB</div>);
1006 });
1007
packages/react-reconciler/src/__tests__/ReactMemo-test.js
+19 -5
@@ -599,7 +599,9 @@ describe('memo', () => {
599 await expect(async () => {
600 await waitForAll([]);
601 }).toErrorDev(
602 - 'Each child in a list should have a unique "key" prop. See https://react.dev/link/warning-keys for more information.\n' +
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 **)',
606 );
607 });
@@ -616,7 +618,10 @@ describe('memo', () => {
618 await expect(async () => {
619 await waitForAll([]);
620 }).toErrorDev(
619 - 'Each child in a list should have a unique "key" prop. See https://react.dev/link/warning-keys for more information.\n' +
621 + 'Each child in a list should have a unique "key" prop.' +
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 **)',
627 );
@@ -636,7 +641,10 @@ describe('memo', () => {
641 await expect(async () => {
642 await waitForAll([]);
643 }).toErrorDev(
639 - 'Each child in a list should have a unique "key" prop. See https://react.dev/link/warning-keys for more information.\n' +
644 + 'Each child in a list should have a unique "key" prop.' +
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 **)',
650 );
@@ -655,7 +663,10 @@ describe('memo', () => {
663 await expect(async () => {
664 await waitForAll([]);
665 }).toErrorDev(
658 - 'Each child in a list should have a unique "key" prop. See https://react.dev/link/warning-keys for more information.\n' +
666 + 'Each child in a list should have a unique "key" prop.' +
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 **)',
672 );
@@ -676,7 +687,10 @@ describe('memo', () => {
687 await expect(async () => {
688 await waitForAll([]);
689 }).toErrorDev(
679 - 'Each child in a list should have a unique "key" prop. See https://react.dev/link/warning-keys for more information.\n' +
690 + 'Each child in a list should have a unique "key" prop.' +
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 **)',
696 );
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
+10 -5
@@ -746,6 +746,10 @@ describe('ReactFlightDOMBrowser', () => {
746 }
747 const Parent = clientExports(ParentClient);
748 const ParentModule = clientExports({Parent: ParentClient});
749 +
750 + const container = document.createElement('div');
751 + const root = ReactDOMClient.createRoot(container);
752 +
753 await expect(async () => {
754 const stream = ReactServerDOMServer.renderToReadableStream(
755 <>
@@ -756,11 +760,12 @@ describe('ReactFlightDOMBrowser', () => {
760 </>,
761 webpackMap,
762 );
759 - await ReactServerDOMClient.createFromReadableStream(stream);
760 - }).toErrorDev(
761 - 'Each child in a list should have a unique "key" prop. ' +
762 - 'See https://react.dev/link/warning-keys for more information.',
763 - );
763 + const result =
764 + await ReactServerDOMClient.createFromReadableStream(stream);
765 + await act(() => {
766 + root.render(result);
767 + });
768 + }).toErrorDev('Each child in a list should have a unique "key" prop.');
769 });
770
771 it('basic use(promise)', async () => {
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js
+70 -2
@@ -256,7 +256,41 @@ describe('ReactFlightDOMEdge', () => {
256 return str;
257 }
258 const element = <ServerComponent />;
259 - const children = new Array(30).fill(element);
259 + // Hardcoded list to avoid the key warning
260 + const children = (
261 + <>
262 + {element}
263 + {element}
264 + {element}
265 + {element}
266 + {element}
267 + {element}
268 + {element}
269 + {element}
270 + {element}
271 + {element}
272 + {element}
273 + {element}
274 + {element}
275 + {element}
276 + {element}
277 + {element}
278 + {element}
279 + {element}
280 + {element}
281 + {element}
282 + {element}
283 + {element}
284 + {element}
285 + {element}
286 + {element}
287 + {element}
288 + {element}
289 + {element}
290 + {element}
291 + {element}
292 + </>
293 + );
294 const resolvedChildren = new Array(30).fill(str);
295 const stream = ReactServerDOMServer.renderToReadableStream(children);
296 const [stream1, stream2] = passThrough(stream).tee();
@@ -288,7 +322,41 @@ describe('ReactFlightDOMEdge', () => {
322 return div;
323 }
324 const element = <ServerComponent />;
291 - const children = new Array(30).fill(element);
325 + // Hardcoded list to avoid the key warning
326 + const children = (
327 + <>
328 + {element}
329 + {element}
330 + {element}
331 + {element}
332 + {element}
333 + {element}
334 + {element}
335 + {element}
336 + {element}
337 + {element}
338 + {element}
339 + {element}
340 + {element}
341 + {element}
342 + {element}
343 + {element}
344 + {element}
345 + {element}
346 + {element}
347 + {element}
348 + {element}
349 + {element}
350 + {element}
351 + {element}
352 + {element}
353 + {element}
354 + {element}
355 + {element}
356 + {element}
357 + {element}
358 + </>
359 + );
360 const resolvedChildren = new Array(30).fill(
361 '<div>this is a long return value</div>',
362 );
packages/react-server/src/ReactFizzServer.js
+75
@@ -337,6 +337,8 @@ export opaque type Request = {
337 onPostpone: (reason: string, postponeInfo: ThrownInfo) => void,
338 // Form state that was the result of an MPA submission, if it was provided.
339 formState: null | ReactFormState<any, any>,
340 + // DEV-only, warning dedupe
341 + didWarnForKey?: null | WeakSet<ComponentStackNode>,
342 };
343
344 // This is a default heuristic for how to split up the HTML content into progressive
@@ -409,6 +411,9 @@ export function createRequest(
411 onFatalError: onFatalError === undefined ? noop : onFatalError,
412 formState: formState === undefined ? null : formState,
413 };
414 + if (__DEV__) {
415 + request.didWarnForKey = null;
416 + }
417 // This segment represents the root fallback.
418 const rootSegment = createPendingSegment(
419 request,
@@ -787,6 +792,19 @@ function createClassComponentStack(
792 };
793 }
794
795 +function createComponentStackFromType(
796 + task: Task,
797 + type: Function | string,
798 +): ComponentStackNode {
799 + if (typeof type === 'string') {
800 + return createBuiltInComponentStack(task, type);
801 + }
802 + if (shouldConstruct(type)) {
803 + return createClassComponentStack(task, type);
804 + }
805 + return createFunctionComponentStack(task, type);
806 +}
807 +
808 type ThrownInfo = {
809 componentStack?: string,
810 };
@@ -2597,6 +2615,59 @@ function replayFragment(
2615 }
2616 }
2617
2618 +function warnForMissingKey(request: Request, task: Task, child: mixed): void {
2619 + if (__DEV__) {
2620 + if (
2621 + child === null ||
2622 + typeof child !== 'object' ||
2623 + (child.$$typeof !== REACT_ELEMENT_TYPE &&
2624 + child.$$typeof !== REACT_PORTAL_TYPE)
2625 + ) {
2626 + return;
2627 + }
2628 +
2629 + if (
2630 + !child._store ||
2631 + ((child._store.validated || child.key != null) &&
2632 + child._store.validated !== 2)
2633 + ) {
2634 + return;
2635 + }
2636 +
2637 + if (typeof child._store !== 'object') {
2638 + throw new Error(
2639 + 'React Component in warnForMissingKey should have a _store. ' +
2640 + 'This error is likely caused by a bug in React. Please file an issue.',
2641 + );
2642 + }
2643 +
2644 + // $FlowFixMe[cannot-write] unable to narrow type from mixed to writable object
2645 + child._store.validated = 1;
2646 +
2647 + let didWarnForKey = request.didWarnForKey;
2648 + if (didWarnForKey == null) {
2649 + didWarnForKey = request.didWarnForKey = new WeakSet();
2650 + }
2651 + const parentStackFrame = task.componentStack;
2652 + if (parentStackFrame === null || didWarnForKey.has(parentStackFrame)) {
2653 + // We already warned for other children in this parent.
2654 + return;
2655 + }
2656 + didWarnForKey.add(parentStackFrame);
2657 +
2658 + // We create a fake component stack for the child to log the stack trace from.
2659 + const stackFrame = createComponentStackFromType(task, (child: any).type);
2660 + task.componentStack = stackFrame;
2661 + console.error(
2662 + 'Each child in a list should have a unique "key" prop.' +
2663 + '%s%s See https://react.dev/link/warning-keys for more information.',
2664 + '',
2665 + '',
2666 + );
2667 + task.componentStack = stackFrame.parent;
2668 + }
2669 +}
2670 +
2671 function renderChildrenArray(
2672 request: Request,
2673 task: Task,
@@ -2618,6 +2689,7 @@ function renderChildrenArray(
2689 return;
2690 }
2691 }
2692 +
2693 const prevTreeContext = task.treeContext;
2694 const totalChildren = children.length;
2695
@@ -2650,6 +2722,9 @@ function renderChildrenArray(
2722
2723 for (let i = 0; i < totalChildren; i++) {
2724 const node = children[i];
2725 + if (__DEV__) {
2726 + warnForMissingKey(request, task, node);
2727 + }
2728 task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i);
2729 // We need to use the non-destructive form so that we can safely pop back
2730 // up and render the sibling if something suspends.
packages/react-server/src/ReactFlightServer.js
+117 -6
@@ -399,6 +399,7 @@ export type Request = {
399 onPostpone: (reason: string) => void,
400 // DEV-only
401 environmentName: string,
402 + didWarnForKey: null | WeakSet<ReactComponentInfo>,
403 };
404
405 const {
@@ -500,6 +501,7 @@ export function createRequest(
501 if (__DEV__) {
502 request.environmentName =
503 environmentName === undefined ? 'Server' : environmentName;
504 + request.didWarnForKey = null;
505 }
506 const rootTask = createTask(request, model, null, false, abortSet);
507 pingedTasks.push(rootTask);
@@ -965,6 +967,7 @@ function renderFunctionComponent<Props>(
967 props: Props,
968 owner: null | ReactComponentInfo, // DEV-only
969 stack: null | string, // DEV-only
970 + validated: number, // DEV-only
971 ): ReactJSONValue {
972 // Reset the task's thenable state before continuing, so that if a later
973 // component suspends we can reuse the same task object. If the same
@@ -1005,6 +1008,10 @@ function renderFunctionComponent<Props>(
1008 // being no references to this as an owner.
1009 outlineModel(request, componentDebugInfo);
1010 emitDebugChunk(request, componentDebugID, componentDebugInfo);
1011 +
1012 + if (enableOwnerStacks) {
1013 + warnForMissingKey(request, key, validated, componentDebugInfo);
1014 + }
1015 }
1016 prepareToUseHooksForComponent(prevThenableState, componentDebugInfo);
1017 result = callComponentInDEV(Component, props, componentDebugInfo);
@@ -1019,6 +1026,23 @@ function renderFunctionComponent<Props>(
1026 // When the return value is in children position we can resolve it immediately,
1027 // to its value without a wrapper if it's synchronously available.
1028 const thenable: Thenable<any> = result;
1029 + if (__DEV__) {
1030 + // If the thenable resolves to an element, then it was in a static position,
1031 + // the return value of a Server Component. That doesn't need further validation
1032 + // of keys. The Server Component itself would have had a key.
1033 + thenable.then(
1034 + resolvedValue => {
1035 + if (
1036 + typeof resolvedValue === 'object' &&
1037 + resolvedValue !== null &&
1038 + resolvedValue.$$typeof === REACT_ELEMENT_TYPE
1039 + ) {
1040 + resolvedValue._store.validated = 1;
1041 + }
1042 + },
1043 + () => {},
1044 + );
1045 + }
1046 if (thenable.status === 'fulfilled') {
1047 return thenable.value;
1048 }
@@ -1102,6 +1126,11 @@ function renderFunctionComponent<Props>(
1126 if (__DEV__) {
1127 (result: any)._debugInfo = iterableChild._debugInfo;
1128 }
1129 + } else if (__DEV__ && (result: any).$$typeof === REACT_ELEMENT_TYPE) {
1130 + // If the server component renders to an element, then it was in a static position.
1131 + // That doesn't need further validation of keys. The Server Component itself would
1132 + // have had a key.
1133 + (result: any)._store.validated = 1;
1134 }
1135 }
1136 // Track this element's key on the Server Component on the keyPath context..
@@ -1124,11 +1153,68 @@ function renderFunctionComponent<Props>(
1153 return json;
1154 }
1155
1156 +function warnForMissingKey(
1157 + request: Request,
1158 + key: null | string,
1159 + validated: number,
1160 + componentDebugInfo: ReactComponentInfo,
1161 +): void {
1162 + if (__DEV__) {
1163 + if (validated !== 2) {
1164 + return;
1165 + }
1166 +
1167 + let didWarnForKey = request.didWarnForKey;
1168 + if (didWarnForKey == null) {
1169 + didWarnForKey = request.didWarnForKey = new WeakSet();
1170 + }
1171 + const parentOwner = componentDebugInfo.owner;
1172 + if (parentOwner != null) {
1173 + if (didWarnForKey.has(parentOwner)) {
1174 + // We already warned for other children in this parent.
1175 + return;
1176 + }
1177 + didWarnForKey.add(parentOwner);
1178 + }
1179 +
1180 + // Call with the server component as the currently rendering component
1181 + // for context.
1182 + callComponentInDEV(
1183 + () => {
1184 + console.error(
1185 + 'Each child in a list should have a unique "key" prop.' +
1186 + '%s%s See https://react.dev/link/warning-keys for more information.',
1187 + '',
1188 + '',
1189 + );
1190 + },
1191 + null,
1192 + componentDebugInfo,
1193 + );
1194 + }
1195 +}
1196 +
1197 function renderFragment(
1198 request: Request,
1199 task: Task,
1200 children: $ReadOnlyArray<ReactClientValue>,
1201 ): ReactJSONValue {
1202 + if (__DEV__) {
1203 + for (let i = 0; i < children.length; i++) {
1204 + const child = children[i];
1205 + if (
1206 + child !== null &&
1207 + typeof child === 'object' &&
1208 + child.$$typeof === REACT_ELEMENT_TYPE
1209 + ) {
1210 + const element: ReactElement = (child: any);
1211 + if (element.key === null && !element._store.validated) {
1212 + element._store.validated = 2;
1213 + }
1214 + }
1215 + }
1216 + }
1217 +
1218 if (task.keyPath !== null) {
1219 // We have a Server Component that specifies a key but we're now splitting
1220 // the tree using a fragment.
@@ -1231,6 +1317,7 @@ function renderClientElement(
1317 props: any,
1318 owner: null | ReactComponentInfo, // DEV-only
1319 stack: null | string, // DEV-only
1320 + validated: number, // DEV-only
1321 ): ReactJSONValue {
1322 // We prepend the terminal client element that actually gets serialized with
1323 // the keys of any Server Components which are not serialized.
@@ -1242,7 +1329,7 @@ function renderClientElement(
1329 }
1330 const element = __DEV__
1331 ? enableOwnerStacks
1245 - ? [REACT_ELEMENT_TYPE, type, key, props, owner, stack]
1332 + ? [REACT_ELEMENT_TYPE, type, key, props, owner, stack, validated]
1333 : [REACT_ELEMENT_TYPE, type, key, props, owner]
1334 : [REACT_ELEMENT_TYPE, type, key, props];
1335 if (task.implicitSlot && key !== null) {
@@ -1292,6 +1379,7 @@ function renderElement(
1379 props: any,
1380 owner: null | ReactComponentInfo, // DEV only
1381 stack: null | string, // DEV only
1382 + validated: number, // DEV only
1383 ): ReactJSONValue {
1384 if (ref !== null && ref !== undefined) {
1385 // When the ref moves to the regular props object this will implicitly
@@ -1312,7 +1400,15 @@ function renderElement(
1400 if (typeof type === 'function') {
1401 if (isClientReference(type) || isOpaqueTemporaryReference(type)) {
1402 // This is a reference to a Client Component.
1315 - return renderClientElement(task, type, key, props, owner, stack);
1403 + return renderClientElement(
1404 + task,
1405 + type,
1406 + key,
1407 + props,
1408 + owner,
1409 + stack,
1410 + validated,
1411 + );
1412 }
1413 // This is a Server Component.
1414 return renderFunctionComponent(
@@ -1323,10 +1419,11 @@ function renderElement(
1419 props,
1420 owner,
1421 stack,
1422 + validated,
1423 );
1424 } else if (typeof type === 'string') {
1425 // This is a host element. E.g. HTML.
1329 - return renderClientElement(task, type, key, props, owner, stack);
1426 + return renderClientElement(task, type, key, props, owner, stack, validated);
1427 } else if (typeof type === 'symbol') {
1428 if (type === REACT_FRAGMENT_TYPE && key === null) {
1429 // For key-less fragments, we add a small optimization to avoid serializing
@@ -1347,11 +1444,19 @@ function renderElement(
1444 }
1445 // This might be a built-in React component. We'll let the client decide.
1446 // Any built-in works as long as its props are serializable.
1350 - return renderClientElement(task, type, key, props, owner, stack);
1447 + return renderClientElement(task, type, key, props, owner, stack, validated);
1448 } else if (type != null && typeof type === 'object') {
1449 if (isClientReference(type)) {
1450 // This is a reference to a Client Component.
1354 - return renderClientElement(task, type, key, props, owner, stack);
1451 + return renderClientElement(
1452 + task,
1453 + type,
1454 + key,
1455 + props,
1456 + owner,
1457 + stack,
1458 + validated,
1459 + );
1460 }
1461 switch (type.$$typeof) {
1462 case REACT_LAZY_TYPE: {
@@ -1372,6 +1477,7 @@ function renderElement(
1477 props,
1478 owner,
1479 stack,
1480 + validated,
1481 );
1482 }
1483 case REACT_FORWARD_REF_TYPE: {
@@ -1383,6 +1489,7 @@ function renderElement(
1489 props,
1490 owner,
1491 stack,
1492 + validated,
1493 );
1494 }
1495 case REACT_MEMO_TYPE: {
@@ -1395,6 +1502,7 @@ function renderElement(
1502 props,
1503 owner,
1504 stack,
1505 + validated,
1506 );
1507 }
1508 }
@@ -1963,8 +2071,11 @@ function renderModelDestructive(
2071 props,
2072 __DEV__ ? element._owner : null,
2073 __DEV__ && enableOwnerStacks
1966 - ? filterDebugStack(element._debugStack)
2074 + ? !element._debugStack || typeof element._debugStack === 'string'
2075 + ? element._debugStack
2076 + : filterDebugStack(element._debugStack)
2077 : null,
2078 + __DEV__ && enableOwnerStacks ? element._store.validated : 0,
2079 );
2080 }
2081 case REACT_LAZY_TYPE: {
packages/react/src/ReactChildren.js
+18 -4
@@ -207,17 +207,20 @@ function mapIntoArray(
207 // The `if` statement here prevents auto-disabling of the safe
208 // coercion ESLint rule, so we must manually disable it below.
209 // $FlowFixMe[incompatible-type] Flow incorrectly thinks React.Portal doesn't have a key
210 - if (mappedChild.key && (!child || child.key !== mappedChild.key)) {
211 - checkKeyStringCoercion(mappedChild.key);
210 + if (mappedChild.key != null) {
211 + if (!child || child.key !== mappedChild.key) {
212 + checkKeyStringCoercion(mappedChild.key);
213 + }
214 }
215 }
214 - mappedChild = cloneAndReplaceKey(
216 + const newChild = cloneAndReplaceKey(
217 mappedChild,
218 // Keep both the (mapped) and old keys if they differ, just as
219 // traverseAllChildren used to do for objects as children
220 escapedPrefix +
221 // $FlowFixMe[incompatible-type] Flow incorrectly thinks React.Portal doesn't have a key
220 - (mappedChild.key && (!child || child.key !== mappedChild.key)
222 + (mappedChild.key != null &&
223 + (!child || child.key !== mappedChild.key)
224 ? escapeUserProvidedKey(
225 // $FlowFixMe[unsafe-addition]
226 '' + mappedChild.key, // eslint-disable-line react-internal/safe-string-coercion
@@ -225,6 +228,17 @@ function mapIntoArray(
228 : '') +
229 childKey,
230 );
231 + if (__DEV__) {
232 + if (nameSoFar !== '' && mappedChild.key == null) {
233 + // We need to validate that this child should have had a key before assigning it one.
234 + if (!newChild._store.validated) {
235 + // We mark this child as having failed validation but we let the actual renderer
236 + // print the warning later.
237 + newChild._store.validated = 2;
238 + }
239 + }
240 + }
241 + mappedChild = newChild;
242 }
243 array.push(mappedChild);
244 }
packages/react/src/ReactSharedInternalsClient.js
-24
@@ -34,9 +34,7 @@ export type SharedStateClient = {
34 thrownErrors: Array<mixed>,
35
36 // ReactDebugCurrentFrame
37 - setExtraStackFrame: (stack: null | string) => void,
37 getCurrentStack: null | (() => string),
39 - getStackAddendum: () => string,
38 };
39
40 export type RendererTask = boolean => RendererTask | null;
@@ -53,30 +51,8 @@ if (__DEV__) {
51 ReactSharedInternals.didScheduleLegacyUpdate = false;
52 ReactSharedInternals.didUsePromise = false;
53 ReactSharedInternals.thrownErrors = [];
56 -
57 - let currentExtraStackFrame = (null: null | string);
58 - ReactSharedInternals.setExtraStackFrame = function (stack: null | string) {
59 - currentExtraStackFrame = stack;
60 - };
54 // Stack implementation injected by the current renderer.
55 ReactSharedInternals.getCurrentStack = (null: null | (() => string));
63 -
64 - ReactSharedInternals.getStackAddendum = function (): string {
65 - let stack = '';
66 -
67 - // Add an extra top frame while an element is being validated
68 - if (currentExtraStackFrame) {
69 - stack += currentExtraStackFrame;
70 - }
71 -
72 - // Delegate to the injected renderer-specific implementation
73 - const impl = ReactSharedInternals.getCurrentStack;
74 - if (impl) {
75 - stack += impl() || '';
76 - }
77 -
78 - return stack;
79 - };
56 }
57
58 export default ReactSharedInternals;
packages/react/src/ReactSharedInternalsServer.js
-23
@@ -38,9 +38,7 @@ export type SharedStateServer = {
38 // DEV-only
39
40 // ReactDebugCurrentFrame
41 - setExtraStackFrame: (stack: null | string) => void,
41 getCurrentStack: null | (() => string),
43 - getStackAddendum: () => string,
42 };
43
44 export type RendererTask = boolean => RendererTask | null;
@@ -59,29 +57,8 @@ if (enableTaint) {
57 }
58
59 if (__DEV__) {
62 - let currentExtraStackFrame = (null: null | string);
63 - ReactSharedInternals.setExtraStackFrame = function (stack: null | string) {
64 - currentExtraStackFrame = stack;
65 - };
60 // Stack implementation injected by the current renderer.
61 ReactSharedInternals.getCurrentStack = (null: null | (() => string));
68 -
69 - ReactSharedInternals.getStackAddendum = function (): string {
70 - let stack = '';
71 -
72 - // Add an extra top frame while an element is being validated
73 - if (currentExtraStackFrame) {
74 - stack += currentExtraStackFrame;
75 - }
76 -
77 - // Delegate to the injected renderer-specific implementation
78 - const impl = ReactSharedInternals.getCurrentStack;
79 - if (impl) {
80 - stack += impl() || '';
81 - }
82 -
83 - return stack;
84 - };
62 }
63
64 export default ReactSharedInternals;
packages/react/src/__tests__/ReactChildren-test.js
+30 -11
@@ -301,7 +301,7 @@ describe('ReactChildren', () => {
301 ]);
302 });
303
304 - it('should be called for each child in an iterable without keys', () => {
304 + it('should be called for each child in an iterable without keys', async () => {
305 const threeDivIterable = {
306 '@@iterator': function () {
307 let i = 0;
@@ -323,11 +323,6 @@ describe('ReactChildren', () => {
323 return kid;
324 });
325
326 - let instance;
327 - expect(() => (instance = <div>{threeDivIterable}</div>)).toErrorDev(
328 - 'Warning: Each child in a list should have a unique "key" prop.',
329 - );
330 -
326 function assertCalls() {
327 expect(callback).toHaveBeenCalledTimes(3);
328 expect(callback).toHaveBeenCalledWith(<div />, 0);
@@ -336,7 +331,18 @@ describe('ReactChildren', () => {
331 callback.mockClear();
332 }
333
334 + let instance;
335 + expect(() => {
336 + instance = <div>{threeDivIterable}</div>;
337 + }).toErrorDev(
338 + // With the flag on this doesn't warn eagerly but only when rendered
339 + gate(flag => flag.enableOwnerStacks)
340 + ? []
341 + : ['Warning: Each child in a list should have a unique "key" prop.'],
342 + );
343 +
344 React.Children.forEach(instance.props.children, callback, context);
345 +
346 assertCalls();
347
348 const mappedChildren = React.Children.map(
@@ -350,6 +356,16 @@ describe('ReactChildren', () => {
356 <div key=".1" />,
357 <div key=".2" />,
358 ]);
359 +
360 + const container = document.createElement('div');
361 + const root = ReactDOMClient.createRoot(container);
362 + await expect(async () => {
363 + await act(() => {
364 + root.render(instance);
365 + });
366 + }).toErrorDev(
367 + 'Warning: Each child in a list should have a unique "key" prop.',
368 + );
369 });
370
371 it('should be called for each child in an iterable with keys', () => {
@@ -953,7 +969,7 @@ describe('ReactChildren', () => {
969 });
970
971 it('should render React.lazy after suspending', async () => {
956 - const lazyElement = React.lazy(async () => ({default: <div />}));
972 + const lazyElement = React.lazy(async () => ({default: <div key="hi" />}));
973 function Component() {
974 return React.Children.map([lazyElement], c =>
975 React.cloneElement(c, {children: 'hi'}),
@@ -969,7 +985,7 @@ describe('ReactChildren', () => {
985 });
986
987 it('should render cached Promises after suspending', async () => {
972 - const promise = Promise.resolve(<div />);
988 + const promise = Promise.resolve(<div key="hi" />);
989 function Component() {
990 return React.Children.map([promise], c =>
991 React.cloneElement(c, {children: 'hi'}),
@@ -1015,7 +1031,9 @@ describe('ReactChildren', () => {
1031 }).toErrorDev(
1032 'Warning: ' +
1033 'Each child in a list should have a unique "key" prop.' +
1018 - ' See https://react.dev/link/warning-keys for more information.' +
1034 + '\n\nCheck the top-level render call using <ComponentReturningArray>. It was passed a child from ComponentReturningArray. ' +
1035 + 'See https://react.dev/link/warning-keys for more information.' +
1036 + '\n in div (at **)' +
1037 '\n in ComponentReturningArray (at **)',
1038 );
1039 });
@@ -1044,8 +1062,9 @@ describe('ReactChildren', () => {
1062 }).toErrorDev(
1063 'Warning: ' +
1064 'Each child in a list should have a unique "key" prop.' +
1047 - ' See https://react.dev/link/warning-keys for more information.',
1048 - {withoutStack: true}, // There's nothing on the stack
1065 + '\n\nCheck the top-level render call using <Root>. ' +
1066 + 'See https://react.dev/link/warning-keys for more information.' +
1067 + '\n in div (at **)',
1068 );
1069 });
1070 });
packages/react/src/__tests__/ReactElementClone-test.js
+19 -8
@@ -364,18 +364,29 @@ describe('ReactElementClone', () => {
364 expect(cloneInstance4.props.prop).toBe('newTestKey');
365 });
366
367 - it('warns for keys for arrays of elements in rest args', () => {
368 - expect(() =>
369 - React.cloneElement(<div />, null, [<div />, <div />]),
370 - ).toErrorDev('Each child in a list should have a unique "key" prop.');
367 + it('warns for keys for arrays of elements in rest args', async () => {
368 + const root = ReactDOMClient.createRoot(document.createElement('div'));
369 + await expect(async () => {
370 + await act(() => {
371 + root.render(React.cloneElement(<div />, null, [<div />, <div />]));
372 + });
373 + }).toErrorDev('Each child in a list should have a unique "key" prop.');
374 });
375
373 - it('does not warns for arrays of elements with keys', () => {
374 - React.cloneElement(<div />, null, [<div key="#1" />, <div key="#2" />]);
376 + it('does not warns for arrays of elements with keys', async () => {
377 + const root = ReactDOMClient.createRoot(document.createElement('div'));
378 + await act(() => {
379 + root.render(
380 + React.cloneElement(<div />, null, [<div key="#1" />, <div key="#2" />]),
381 + );
382 + });
383 });
384
377 - it('does not warn when the element is directly in rest args', () => {
378 - React.cloneElement(<div />, null, <div />, <div />);
385 + it('does not warn when the element is directly in rest args', async () => {
386 + const root = ReactDOMClient.createRoot(document.createElement('div'));
387 + await act(() => {
388 + root.render(React.cloneElement(<div />, null, <div />, <div />));
389 + });
390 });
391
392 it('does not warn when the array contains a non-element', () => {
packages/react/src/__tests__/ReactElementValidator-test.internal.js
+202 -75
@@ -30,17 +30,22 @@ describe('ReactElementValidator', () => {
30 act = require('internal-test-utils').act;
31 ComponentClass = class extends React.Component {
32 render() {
33 - return React.createElement('div');
33 + return React.createElement('div', null, this.props.children);
34 }
35 };
36 });
37
38 - it('warns for keys for arrays of elements in rest args', () => {
39 - expect(() => {
40 - React.createElement(ComponentClass, null, [
41 - React.createElement(ComponentClass),
42 - React.createElement(ComponentClass),
43 - ]);
38 + it('warns for keys for arrays of elements in rest args', async () => {
39 + const root = ReactDOMClient.createRoot(document.createElement('div'));
40 + await expect(async () => {
41 + await act(() =>
42 + root.render(
43 + React.createElement(ComponentClass, null, [
44 + React.createElement(ComponentClass),
45 + React.createElement(ComponentClass),
46 + ]),
47 + ),
48 + );
49 }).toErrorDev('Each child in a list should have a unique "key" prop.');
50 });
51
@@ -67,14 +72,18 @@ describe('ReactElementValidator', () => {
72 await act(() => root.render(React.createElement(ComponentWrapper)));
73 }).toErrorDev(
74 'Each child in a list should have a unique "key" prop.' +
70 - '\n\nCheck the render method of `InnerClass`. ' +
75 + '\n\nCheck the render method of `' +
76 + (gate(flags => flags.enableOwnerStacks)
77 + ? 'ComponentClass'
78 + : 'InnerClass') +
79 + '`. ' +
80 'It was passed a child from ComponentWrapper. ',
81 );
82 });
83
84 it('warns for keys for arrays with no owner or parent info', async () => {
76 - function Anonymous() {
77 - return <div />;
85 + function Anonymous({children}) {
86 + return <div>{children}</div>;
87 }
88 Object.defineProperty(Anonymous, 'name', {value: undefined});
89
@@ -84,9 +93,16 @@ describe('ReactElementValidator', () => {
93 const root = ReactDOMClient.createRoot(document.createElement('div'));
94 await act(() => root.render(<Anonymous>{divs}</Anonymous>));
95 }).toErrorDev(
87 - 'Warning: Each child in a list should have a unique ' +
88 - '"key" prop. See https://react.dev/link/warning-keys for more information.\n' +
89 - ' in div (at **)',
96 + gate(flags => flags.enableOwnerStacks)
97 + ? // For owner stacks the parent being validated is the div.
98 + 'Warning: Each child in a list should have a unique ' +
99 + '"key" prop.' +
100 + '\n\nCheck the top-level render call using <div>. ' +
101 + 'See https://react.dev/link/warning-keys for more information.\n' +
102 + ' in div (at **)'
103 + : 'Warning: Each child in a list should have a unique ' +
104 + '"key" prop. See https://react.dev/link/warning-keys for more information.\n' +
105 + ' in div (at **)',
106 );
107 });
108
@@ -126,6 +142,9 @@ 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' : '') +
148 ' in Component (at **)\n' +
149 ' in Parent (at **)\n' +
150 ' in GrandParent (at **)',
@@ -153,7 +172,7 @@ describe('ReactElementValidator', () => {
172 );
173 });
174
156 - it('warns for keys for iterables of elements in rest args', () => {
175 + it('warns for keys for iterables of elements in rest args', async () => {
176 const iterable = {
177 '@@iterator': function () {
178 let i = 0;
@@ -169,9 +188,23 @@ describe('ReactElementValidator', () => {
188 },
189 };
190
172 - expect(() =>
173 - React.createElement(ComponentClass, null, iterable),
174 - ).toErrorDev('Each child in a list should have a unique "key" prop.');
191 + await expect(async () => {
192 + const root = ReactDOMClient.createRoot(document.createElement('div'));
193 + await act(() =>
194 + root.render(React.createElement(ComponentClass, null, iterable)),
195 + );
196 + }).toErrorDev(
197 + gate(flag => flag.enableOwnerStacks)
198 + ? 'Each child in a list should have a unique "key" prop.'
199 + : // Since each pass generates a new element, it doesn't get marked as
200 + // validated and it gets rechecked each time.
201 +
202 + [
203 + 'Each child in a list should have a unique "key" prop.',
204 + 'Each child in a list should have a unique "key" prop.',
205 + 'Each child in a list should have a unique "key" prop.',
206 + ],
207 + );
208 });
209
210 it('does not warns for arrays of elements with keys', () => {
@@ -226,65 +259,154 @@ describe('ReactElementValidator', () => {
259 const root = ReactDOMClient.createRoot(document.createElement('div'));
260 await act(() => root.render(React.createElement(ParentComp)));
261 }).toErrorDev(
229 - 'Each child in a list should have a unique "key" prop. ' +
262 + 'Each child in a list should have a unique "key" prop.' +
263 + '\n\nCheck the render method of `ParentComp`. It was passed a child from MyComp. ' +
264 '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.
267 + ' in div (at **)\n' +
268 ' in MyComp (at **)\n' +
269 ' in ParentComp (at **)',
270 );
271 });
272
236 - it('gives a helpful error when passing invalid types', () => {
273 + it('gives a helpful error when passing invalid types', async () => {
274 function Foo() {}
238 - expect(() => {
239 - React.createElement(undefined);
240 - React.createElement(null);
241 - React.createElement(true);
242 - React.createElement({x: 17});
243 - React.createElement({});
244 - React.createElement(React.createElement('div'));
245 - React.createElement(React.createElement(Foo));
246 - React.createElement(React.createElement(React.createContext().Consumer));
247 - React.createElement({$$typeof: 'non-react-thing'});
275 + const errors = [];
276 + await expect(async () => {
277 + const root = ReactDOMClient.createRoot(document.createElement('div'), {
278 + onUncaughtError(error) {
279 + errors.push(error.message);
280 + },
281 + });
282 + const cases = [
283 + React.createElement(undefined),
284 + React.createElement(null),
285 + React.createElement(true),
286 + React.createElement({x: 17}),
287 + React.createElement({}),
288 + React.createElement(React.createElement('div')),
289 + React.createElement(React.createElement(Foo)),
290 + React.createElement(
291 + React.createElement(React.createContext().Consumer),
292 + ),
293 + React.createElement({$$typeof: 'non-react-thing'}),
294 + ];
295 + for (let i = 0; i < cases.length; i++) {
296 + await act(() => root.render(cases[i]));
297 + }
298 }).toErrorDev(
249 - [
250 - 'Warning: React.createElement: type is invalid -- expected a string ' +
251 - '(for built-in components) or a class/function (for composite ' +
252 - 'components) but got: undefined. You likely forgot to export your ' +
253 - "component from the file it's defined in, or you might have mixed up " +
254 - 'default and named imports.',
255 - 'Warning: React.createElement: type is invalid -- expected a string ' +
256 - '(for built-in components) or a class/function (for composite ' +
257 - 'components) but got: null.',
258 - 'Warning: React.createElement: type is invalid -- expected a string ' +
259 - '(for built-in components) or a class/function (for composite ' +
260 - 'components) but got: boolean.',
261 - 'Warning: React.createElement: type is invalid -- expected a string ' +
262 - '(for built-in components) or a class/function (for composite ' +
263 - 'components) but got: object.',
264 - 'Warning: React.createElement: type is invalid -- expected a string ' +
265 - '(for built-in components) or a class/function (for composite ' +
266 - 'components) but got: object. You likely forgot to export your ' +
267 - "component from the file it's defined in, or you might have mixed up " +
268 - 'default and named imports.',
269 - 'Warning: React.createElement: type is invalid -- expected a string ' +
270 - '(for built-in components) or a class/function (for composite ' +
271 - 'components) but got: <div />. Did you accidentally export a JSX literal ' +
272 - 'instead of a component?',
273 - 'Warning: React.createElement: type is invalid -- expected a string ' +
274 - '(for built-in components) or a class/function (for composite ' +
275 - 'components) but got: <Foo />. Did you accidentally export a JSX literal ' +
276 - 'instead of a component?',
277 - 'Warning: React.createElement: type is invalid -- expected a string ' +
278 - '(for built-in components) or a class/function (for composite ' +
279 - 'components) but got: <Context.Consumer />. Did you accidentally ' +
280 - 'export a JSX literal instead of a component?',
281 - 'Warning: React.createElement: type is invalid -- expected a string ' +
282 - '(for built-in components) or a class/function (for composite ' +
283 - 'components) but got: object.',
284 - ],
299 + gate(flag => flag.enableOwnerStacks)
300 + ? // We don't need these extra warnings because we already have the errors.
301 + []
302 + : [
303 + 'Warning: React.createElement: type is invalid -- expected a string ' +
304 + '(for built-in components) or a class/function (for composite ' +
305 + 'components) but got: undefined. You likely forgot to export your ' +
306 + "component from the file it's defined in, or you might have mixed up " +
307 + 'default and named imports.',
308 + 'Warning: React.createElement: type is invalid -- expected a string ' +
309 + '(for built-in components) or a class/function (for composite ' +
310 + 'components) but got: null.',
311 + 'Warning: React.createElement: type is invalid -- expected a string ' +
312 + '(for built-in components) or a class/function (for composite ' +
313 + 'components) but got: boolean.',
314 + 'Warning: React.createElement: type is invalid -- expected a string ' +
315 + '(for built-in components) or a class/function (for composite ' +
316 + 'components) but got: object.',
317 + 'Warning: React.createElement: type is invalid -- expected a string ' +
318 + '(for built-in components) or a class/function (for composite ' +
319 + 'components) but got: object. You likely forgot to export your ' +
320 + "component from the file it's defined in, or you might have mixed up " +
321 + 'default and named imports.',
322 + 'Warning: React.createElement: type is invalid -- expected a string ' +
323 + '(for built-in components) or a class/function (for composite ' +
324 + 'components) but got: <div />. Did you accidentally export a JSX literal ' +
325 + 'instead of a component?',
326 + 'Warning: React.createElement: type is invalid -- expected a string ' +
327 + '(for built-in components) or a class/function (for composite ' +
328 + 'components) but got: <Foo />. Did you accidentally export a JSX literal ' +
329 + 'instead of a component?',
330 + 'Warning: React.createElement: type is invalid -- expected a string ' +
331 + '(for built-in components) or a class/function (for composite ' +
332 + 'components) but got: <Context.Consumer />. Did you accidentally ' +
333 + 'export a JSX literal instead of a component?',
334 + 'Warning: React.createElement: type is invalid -- expected a string ' +
335 + '(for built-in components) or a class/function (for composite ' +
336 + 'components) but got: object.',
337 + ],
338 {withoutStack: true},
339 );
340
341 + expect(errors).toEqual(
342 + __DEV__
343 + ? [
344 + 'Element type is invalid: expected a string ' +
345 + '(for built-in components) or a class/function (for composite ' +
346 + 'components) but got: undefined. You likely forgot to export your ' +
347 + "component from the file it's defined in, or you might have mixed up " +
348 + 'default and named imports.',
349 + 'Element type is invalid: expected a string ' +
350 + '(for built-in components) or a class/function (for composite ' +
351 + 'components) but got: null.',
352 + 'Element type is invalid: expected a string ' +
353 + '(for built-in components) or a class/function (for composite ' +
354 + 'components) but got: boolean.',
355 + 'Element type is invalid: expected a string ' +
356 + '(for built-in components) or a class/function (for composite ' +
357 + 'components) but got: object.',
358 + 'Element type is invalid: expected a string ' +
359 + '(for built-in components) or a class/function (for composite ' +
360 + 'components) but got: object. You likely forgot to export your ' +
361 + "component from the file it's defined in, or you might have mixed up " +
362 + 'default and named imports.',
363 + 'Element type is invalid: expected a string ' +
364 + '(for built-in components) or a class/function (for composite ' +
365 + 'components) but got: <div />. Did you accidentally export a JSX literal ' +
366 + 'instead of a component?',
367 + 'Element type is invalid: expected a string ' +
368 + '(for built-in components) or a class/function (for composite ' +
369 + 'components) but got: <Foo />. Did you accidentally export a JSX literal ' +
370 + 'instead of a component?',
371 + 'Element type is invalid: expected a string ' +
372 + '(for built-in components) or a class/function (for composite ' +
373 + 'components) but got: <Context.Consumer />. Did you accidentally ' +
374 + 'export a JSX literal instead of a component?',
375 + 'Element type is invalid: expected a string ' +
376 + '(for built-in components) or a class/function (for composite ' +
377 + 'components) but got: object.',
378 + ]
379 + : [
380 + 'Element type is invalid: expected a string ' +
381 + '(for built-in components) or a class/function (for composite ' +
382 + 'components) but got: undefined.',
383 + 'Element type is invalid: expected a string ' +
384 + '(for built-in components) or a class/function (for composite ' +
385 + 'components) but got: null.',
386 + 'Element type is invalid: expected a string ' +
387 + '(for built-in components) or a class/function (for composite ' +
388 + 'components) but got: boolean.',
389 + 'Element type is invalid: expected a string ' +
390 + '(for built-in components) or a class/function (for composite ' +
391 + 'components) but got: object.',
392 + 'Element type is invalid: expected a string ' +
393 + '(for built-in components) or a class/function (for composite ' +
394 + 'components) but got: object.',
395 + 'Element type is invalid: expected a string ' +
396 + '(for built-in components) or a class/function (for composite ' +
397 + 'components) but got: object.',
398 + 'Element type is invalid: expected a string ' +
399 + '(for built-in components) or a class/function (for composite ' +
400 + 'components) but got: object.',
401 + 'Element type is invalid: expected a string ' +
402 + '(for built-in components) or a class/function (for composite ' +
403 + 'components) but got: object.',
404 + 'Element type is invalid: expected a string ' +
405 + '(for built-in components) or a class/function (for composite ' +
406 + 'components) but got: object.',
407 + ],
408 + );
409 +
410 // Should not log any additional warnings
411 React.createElement('div');
412 });
@@ -303,16 +425,21 @@ describe('ReactElementValidator', () => {
425 'or a class/function (for composite components) but got: null.' +
426 (__DEV__ ? '\n\nCheck the render method of `ParentComp`.' : ''),
427 );
306 - }).toErrorDev([
307 - 'Warning: React.createElement: type is invalid -- expected a string ' +
308 - '(for built-in components) or a class/function (for composite ' +
309 - 'components) but got: null.\n' +
310 - ' in ParentComp (at **)',
311 - 'Warning: React.createElement: type is invalid -- expected a string ' +
312 - '(for built-in components) or a class/function (for composite ' +
313 - 'components) but got: null.\n' +
314 - ' in ParentComp (at **)',
315 - ]);
428 + }).toErrorDev(
429 + gate(flag => flag.enableOwnerStacks)
430 + ? // We don't need these extra warnings because we already have the errors.
431 + []
432 + : [
433 + 'Warning: React.createElement: type is invalid -- expected a string ' +
434 + '(for built-in components) or a class/function (for composite ' +
435 + 'components) but got: null.\n' +
436 + ' in ParentComp (at **)',
437 + 'Warning: React.createElement: type is invalid -- expected a string ' +
438 + '(for built-in components) or a class/function (for composite ' +
439 + 'components) but got: null.\n' +
440 + ' in ParentComp (at **)',
441 + ],
442 + );
443 });
444
445 it('warns for fragments with illegal attributes', async () => {
packages/react/src/__tests__/ReactJSXElementValidator-test.js
+25 -9
@@ -28,7 +28,7 @@ describe('ReactJSXElementValidator', () => {
28
29 Component = class extends React.Component {
30 render() {
31 - return <div />;
31 + return <div>{this.props.children}</div>;
32 }
33 };
34
@@ -72,7 +72,11 @@ describe('ReactJSXElementValidator', () => {
72 });
73 }).toErrorDev([
74 'Each child in a list should have a unique "key" prop.' +
75 - '\n\nCheck the render method of `InnerComponent`. ' +
75 + '\n\nCheck the render method of `' +
76 + (gate(flag => flag.enableOwnerStacks)
77 + ? 'Component'
78 + : 'InnerComponent') +
79 + '`. ' +
80 'It was passed a child from ComponentWrapper. ',
81 ]);
82 });
@@ -97,7 +101,17 @@ describe('ReactJSXElementValidator', () => {
101 await act(() => {
102 root.render(<Component>{iterable}</Component>);
103 });
100 - }).toErrorDev('Each child in a list should have a unique "key" prop.');
104 + }).toErrorDev(
105 + gate(flag => flag.enableOwnerStacks)
106 + ? ['Each child in a list should have a unique "key" prop.']
107 + : // Since each pass generates a new element, it doesn't get marked as
108 + // validated and it gets rechecked each time.
109 + [
110 + 'Each child in a list should have a unique "key" prop.',
111 + 'Each child in a list should have a unique "key" prop.',
112 + 'Each child in a list should have a unique "key" prop.',
113 + ],
114 + );
115 });
116
117 it('does not warn for arrays of elements with keys', async () => {
@@ -151,11 +165,9 @@ describe('ReactJSXElementValidator', () => {
165 };
166 iterable.entries = iterable['@@iterator'];
167
154 - const container = document.createElement('div');
155 - const root = ReactDOMClient.createRoot(container);
156 - await act(() => {
157 - root.render(<Component>{iterable}</Component>);
158 - });
168 + // This only applies to the warning during construction.
169 + // We do warn if it's actually rendered.
170 + <Component>{iterable}</Component>;
171 });
172
173 it('does not warn when the element is directly as children', async () => {
@@ -194,8 +206,12 @@ describe('ReactJSXElementValidator', () => {
206 root.render(<ParentComp />);
207 });
208 }).toErrorDev(
197 - 'Each child in a list should have a unique "key" prop. ' +
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.
214 + ' in div (at **)\n' +
215 ' in MyComp (at **)\n' +
216 ' in ParentComp (at **)',
217 );
packages/react/src/__tests__/ReactJSXRuntime-test.js
+3
@@ -300,6 +300,9 @@ describe('ReactJSXRuntime', () => {
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' : '') +
306 ' in Parent (at **)',
307 );
308 });
packages/react/src/__tests__/forwardRef-test.js
+20 -5
@@ -193,7 +193,10 @@ describe('forwardRef', () => {
193 await expect(async () => {
194 await waitForAll([]);
195 }).toErrorDev(
196 - 'Each child in a list should have a unique "key" prop. See https://react.dev/link/warning-keys for more information.\n' +
196 + 'Each child in a list should have a unique "key" prop.' +
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 **)',
201 );
202 });
@@ -210,7 +213,10 @@ describe('forwardRef', () => {
213 await expect(async () => {
214 await waitForAll([]);
215 }).toErrorDev(
213 - 'Each child in a list should have a unique "key" prop. See https://react.dev/link/warning-keys for more information.\n' +
216 + 'Each child in a list should have a unique "key" prop.' +
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 **)',
222 );
@@ -230,7 +236,10 @@ describe('forwardRef', () => {
236 await expect(async () => {
237 await waitForAll([]);
238 }).toErrorDev(
233 - 'Each child in a list should have a unique "key" prop. See https://react.dev/link/warning-keys for more information.\n' +
239 + 'Each child in a list should have a unique "key" prop.' +
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 **)',
245 );
@@ -249,7 +258,10 @@ describe('forwardRef', () => {
258 await expect(async () => {
259 await waitForAll([]);
260 }).toErrorDev(
252 - 'Each child in a list should have a unique "key" prop. See https://react.dev/link/warning-keys for more information.\n' +
261 + 'Each child in a list should have a unique "key" prop.' +
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 **)',
267 );
@@ -270,7 +282,10 @@ describe('forwardRef', () => {
282 await expect(async () => {
283 await waitForAll([]);
284 }).toErrorDev(
273 - 'Each child in a list should have a unique "key" prop. See https://react.dev/link/warning-keys for more information.\n' +
285 + 'Each child in a list should have a unique "key" prop.' +
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 **)',
291 );
packages/react/src/jsx/ReactJSXElement.js
+38 -80
@@ -342,7 +342,7 @@ function ReactElement(
342 configurable: false,
343 enumerable: false,
344 writable: true,
345 - value: false,
345 + value: 0,
346 });
347 // debugInfo contains Server Component debug information.
348 Object.defineProperty(element, '_debugInfo', {
@@ -708,7 +708,7 @@ export function jsxDEV(type, config, maybeKey, isStaticChildren, source, self) {
708 }
709 }
710
711 - const element = ReactElement(
711 + return ReactElement(
712 type,
713 key,
714 ref,
@@ -719,12 +719,6 @@ export function jsxDEV(type, config, maybeKey, isStaticChildren, source, self) {
719 __DEV__ && enableOwnerStacks ? Error('react-stack-top-frame') : undefined,
720 __DEV__ && enableOwnerStacks ? createTask(getTaskName(type)) : undefined,
721 );
722 -
723 - if (type === REACT_FRAGMENT_TYPE) {
724 - validateFragmentProps(element);
725 - }
726 -
727 - return element;
722 }
723 }
724
@@ -734,7 +728,12 @@ export function jsxDEV(type, config, maybeKey, isStaticChildren, source, self) {
728 */
729 export function createElement(type, config, children) {
730 if (__DEV__) {
737 - if (!isValidElementType(type)) {
731 + if (!enableOwnerStacks && !isValidElementType(type)) {
732 + // This is just an optimistic check that provides a better stack trace before
733 + // owner stacks. It's really up to the renderer if it's a valid element type.
734 + // When owner stacks are enabled, we instead warn in the renderer and it'll
735 + // have the stack trace of the JSX element anyway.
736 + //
737 // This is an invalid element type.
738 //
739 // We warn in this case but don't throw. We expect the element creation to
@@ -900,7 +899,7 @@ export function createElement(type, config, children) {
899 }
900 }
901
903 - const element = ReactElement(
902 + return ReactElement(
903 type,
904 key,
905 ref,
@@ -911,12 +910,6 @@ export function createElement(type, config, children) {
910 __DEV__ && enableOwnerStacks ? Error('react-stack-top-frame') : undefined,
911 __DEV__ && enableOwnerStacks ? createTask(getTaskName(type)) : undefined,
912 );
914 -
915 - if (type === REACT_FRAGMENT_TYPE) {
916 - validateFragmentProps(element);
917 - }
918 -
919 - return element;
913 }
914
915 export function cloneAndReplaceKey(oldElement, newKey) {
@@ -1054,19 +1047,6 @@ export function cloneElement(element, config, children) {
1047 return clonedElement;
1048 }
1049
1057 -function getDeclarationErrorAddendum() {
1058 - if (__DEV__) {
1059 - const owner = getOwner();
1060 - if (owner) {
1061 - const name = getComponentNameFromType(owner.type);
1062 - if (name) {
1063 - return '\n\nCheck the render method of `' + name + '`.';
1064 - }
1065 - }
1066 - return '';
1067 - }
1068 -}
1069 -
1050 /**
1051 * Ensure that every element either is passed in a static location, in an
1052 * array with an explicit keys property defined, or in an object literal
@@ -1093,7 +1073,7 @@ function validateChildKeys(node, parentType) {
1073 } else if (isValidElement(node)) {
1074 // This element was passed in a valid location.
1075 if (node._store) {
1096 - node._store.validated = true;
1076 + node._store.validated = 1;
1077 }
1078 } else {
1079 const iteratorFn = getIteratorFn(node);
@@ -1145,11 +1125,15 @@ const ownerHasKeyUseWarning = {};
1125 * @param {*} parentType element's parent's type.
1126 */
1127 function validateExplicitKey(element, parentType) {
1128 + if (enableOwnerStacks) {
1129 + // Skip. Will verify in renderer instead.
1130 + return;
1131 + }
1132 if (__DEV__) {
1133 if (!element._store || element._store.validated || element.key != null) {
1134 return;
1135 }
1152 - element._store.validated = true;
1136 + element._store.validated = 1;
1137
1138 const currentComponentErrorInfo = getCurrentComponentErrorInfo(parentType);
1139 if (ownerHasKeyUseWarning[currentComponentErrorInfo]) {
@@ -1172,36 +1156,40 @@ function validateExplicitKey(element, parentType) {
1156 childOwner = ` It was passed a child from ${ownerName}.`;
1157 }
1158
1175 - setCurrentlyValidatingElement(element);
1159 + const prevGetCurrentStack = ReactSharedInternals.getCurrentStack;
1160 + ReactSharedInternals.getCurrentStack = function () {
1161 + const owner = element._owner;
1162 + // Add an extra top frame while an element is being validated
1163 + let stack = describeUnknownElementTypeFrameInDEV(
1164 + element.type,
1165 + owner ? owner.type : null,
1166 + );
1167 + // Delegate to the injected renderer-specific implementation
1168 + if (prevGetCurrentStack) {
1169 + stack += prevGetCurrentStack() || '';
1170 + }
1171 + return stack;
1172 + };
1173 console.error(
1174 'Each child in a list should have a unique "key" prop.' +
1175 '%s%s See https://react.dev/link/warning-keys for more information.',
1176 currentComponentErrorInfo,
1177 childOwner,
1178 );
1182 - setCurrentlyValidatingElement(null);
1183 - }
1184 -}
1185 -
1186 -function setCurrentlyValidatingElement(element) {
1187 - if (__DEV__) {
1188 - if (element) {
1189 - const owner = element._owner;
1190 - const stack = describeUnknownElementTypeFrameInDEV(
1191 - element.type,
1192 - owner ? owner.type : null,
1193 - );
1194 - ReactSharedInternals.setExtraStackFrame(stack);
1195 - } else {
1196 - ReactSharedInternals.setExtraStackFrame(null);
1197 - }
1179 + ReactSharedInternals.getCurrentStack = prevGetCurrentStack;
1180 }
1181 }
1182
1183 function getCurrentComponentErrorInfo(parentType) {
1184 if (__DEV__) {
1203 - let info = getDeclarationErrorAddendum();
1204 -
1185 + let info = '';
1186 + const owner = getOwner();
1187 + if (owner) {
1188 + const name = getComponentNameFromType(owner.type);
1189 + if (name) {
1190 + info = '\n\nCheck the render method of `' + name + '`.';
1191 + }
1192 + }
1193 if (!info) {
1194 const parentName = getComponentNameFromType(parentType);
1195 if (parentName) {
@@ -1212,36 +1200,6 @@ function getCurrentComponentErrorInfo(parentType) {
1200 }
1201 }
1202
1215 -/**
1216 - * Given a fragment, validate that it can only be provided with fragment props
1217 - * @param {ReactElement} fragment
1218 - */
1219 -function validateFragmentProps(fragment) {
1220 - // TODO: Move this to render phase instead of at element creation.
1221 - if (__DEV__) {
1222 - const keys = Object.keys(fragment.props);
1223 - for (let i = 0; i < keys.length; i++) {
1224 - const key = keys[i];
1225 - if (key !== 'children' && key !== 'key') {
1226 - setCurrentlyValidatingElement(fragment);
1227 - console.error(
1228 - 'Invalid prop `%s` supplied to `React.Fragment`. ' +
1229 - 'React.Fragment can only have `key` and `children` props.',
1230 - key,
1231 - );
1232 - setCurrentlyValidatingElement(null);
1233 - break;
1234 - }
1235 - }
1236 -
1237 - if (!enableRefAsProp && fragment.ref !== null) {
1238 - setCurrentlyValidatingElement(fragment);
1239 - console.error('Invalid attribute `ref` supplied to `React.Fragment`.');
1240 - setCurrentlyValidatingElement(null);
1241 - }
1242 - }
1243 -}
1244 -
1203 function coerceStringRef(mixedRef, owner, type) {
1204 if (disableStringRefs) {
1205 return mixedRef;
packages/shared/ReactComponentStackFrame.js
+1
@@ -315,6 +315,7 @@ function shouldConstruct(Component: Function) {
315 return !!(prototype && prototype.isReactComponent);
316 }
317
318 +// TODO: Delete this once the key warning no longer uses it. I.e. when enableOwnerStacks ship.
319 export function describeUnknownElementTypeFrameInDEV(type: any): string {
320 if (!__DEV__) {
321 return '';
packages/shared/ReactElementType.js
+1 -1
@@ -23,7 +23,7 @@ export type ReactElement = {
23 _owner: any,
24
25 // __DEV__
26 - _store: {validated: boolean, ...},
26 + _store: {validated: 0 | 1 | 2, ...}, // 0: not validated, 1: validated, 2: force fail
27 _debugInfo: null | ReactDebugInfo,
28 _debugStack: Error,
29 _debugTask: null | ConsoleTask,
packages/shared/consoleWithStackDev.js
+6 -4
@@ -43,10 +43,12 @@ function printWarning(level, format, args) {
43 const isErrorLogger =
44 format === '%s\n\n%s\n' || format === '%o\n\n%s\n\n%s\n';
45
46 - const stack = ReactSharedInternals.getStackAddendum();
47 - if (stack !== '') {
48 - format += '%s';
49 - args = args.concat([stack]);
46 + if (ReactSharedInternals.getCurrentStack) {
47 + const stack = ReactSharedInternals.getCurrentStack();
48 + if (stack !== '') {
49 + format += '%s';
50 + args = args.concat([stack]);
51 + }
52 }
53
54 if (isErrorLogger) {
packages/shared/forks/consoleWithStackDev.www.js
+2 -2
@@ -37,8 +37,8 @@ function printWarning(level, format, args) {
37 const ReactSharedInternals =
38 React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
39 // Defensive in case this is fired before React is initialized.
40 - if (ReactSharedInternals != null) {
41 - const stack = ReactSharedInternals.getStackAddendum();
40 + if (ReactSharedInternals != null && ReactSharedInternals.getCurrentStack) {
41 + const stack = ReactSharedInternals.getCurrentStack();
42 if (stack !== '') {
43 format += '%s';
44 args.push(stack);
packages/shared/isValidElementType.js
+2
@@ -35,6 +35,8 @@ import {
35
36 const REACT_CLIENT_REFERENCE: symbol = Symbol.for('react.client.reference');
37
38 +// This function is deprecated. Don't use. Only the renderer knows what a valid type is.
39 +// TODO: Delete this when enableOwnerStacks ships.
40 export default function isValidElementType(type: mixed): boolean {
41 if (typeof type === 'string' || typeof type === 'function') {
42 return true;