@@ -399,6 +399,7 @@ export type Request = {
399399 onPostpone : ( reason : string ) => void ,
400400 // DEV-only
401401 environmentName : string ,
402+ didWarnForKey : null | WeakSet < ReactComponentInfo > ,
402403} ;
403404
404405const {
@@ -500,6 +501,7 @@ export function createRequest(
500501 if ( __DEV__ ) {
501502 request . environmentName =
502503 environmentName === undefined ? 'Server' : environmentName ;
504+ request . didWarnForKey = null ;
503505 }
504506 const rootTask = createTask ( request , model , null , false , abortSet ) ;
505507 pingedTasks . push ( rootTask ) ;
@@ -965,6 +967,7 @@ function renderFunctionComponent<Props>(
965967 props : Props ,
966968 owner : null | ReactComponentInfo , // DEV-only
967969 stack : null | string , // DEV-only
970+ validated : number , // DEV-only
968971) : ReactJSONValue {
969972 // Reset the task's thenable state before continuing, so that if a later
970973 // component suspends we can reuse the same task object. If the same
@@ -1005,6 +1008,10 @@ function renderFunctionComponent<Props>(
10051008 // being no references to this as an owner.
10061009 outlineModel ( request , componentDebugInfo ) ;
10071010 emitDebugChunk ( request , componentDebugID , componentDebugInfo ) ;
1011+
1012+ if ( enableOwnerStacks ) {
1013+ warnForMissingKey ( request , key , validated , componentDebugInfo ) ;
1014+ }
10081015 }
10091016 prepareToUseHooksForComponent(prevThenableState, componentDebugInfo);
10101017 result = callComponentInDEV(Component, props, componentDebugInfo);
@@ -1102,6 +1109,8 @@ function renderFunctionComponent<Props>(
11021109 if ( __DEV__ ) {
11031110 ( result : any ) . _debugInfo = iterableChild . _debugInfo ;
11041111 }
1112+ } else if ( __DEV__ && ( result : any ) . $$typeof === REACT_ELEMENT_TYPE ) {
1113+ ( result : any ) . _store . validated = 1 ;
11051114 }
11061115 }
11071116 // Track this element's key on the Server Component on the keyPath context..
@@ -1124,11 +1133,68 @@ function renderFunctionComponent<Props>(
11241133 return json;
11251134}
11261135
1136+ function warnForMissingKey (
1137+ request : Request ,
1138+ key : null | string ,
1139+ validated : number ,
1140+ componentDebugInfo : ReactComponentInfo ,
1141+ ) : void {
1142+ if ( __DEV__ ) {
1143+ if ( validated !== 2 ) {
1144+ return ;
1145+ }
1146+
1147+ let didWarnForKey = request.didWarnForKey;
1148+ if (didWarnForKey == null) {
1149+ didWarnForKey = request . didWarnForKey = new WeakSet ( ) ;
1150+ }
1151+ const parentOwner = componentDebugInfo.owner;
1152+ if (parentOwner != null) {
1153+ if ( didWarnForKey . has ( parentOwner ) ) {
1154+ // We already warned for other children in this parent.
1155+ return ;
1156+ }
1157+ didWarnForKey.add(parentOwner);
1158+ }
1159+
1160+ // Call with the server component as the currently rendering component
1161+ // for context.
1162+ callComponentInDEV (
1163+ ( ) => {
1164+ console . error (
1165+ 'Each child in a list should have a unique "key" prop.' +
1166+ '%s%s See https://react.dev/link/warning-keys for more information.' ,
1167+ '' ,
1168+ '' ,
1169+ ) ;
1170+ } ,
1171+ null,
1172+ componentDebugInfo,
1173+ );
1174+ }
1175+ }
1176+
11271177function renderFragment (
11281178 request : Request ,
11291179 task : Task ,
11301180 children : $ReadOnlyArray < ReactClientValue > ,
11311181): ReactJSONValue {
1182+ if ( __DEV__ ) {
1183+ for ( let i = 0 ; i < children . length ; i ++ ) {
1184+ const child = children [ i ] ;
1185+ if (
1186+ child !== null &&
1187+ typeof child === 'object' &&
1188+ child . $$typeof === REACT_ELEMENT_TYPE
1189+ ) {
1190+ const element : ReactElement = ( child : any ) ;
1191+ if ( element . key === null && ! element . _store . validated ) {
1192+ element . _store . validated = 2 ;
1193+ }
1194+ }
1195+ }
1196+ }
1197+
11321198 if ( task . keyPath !== null ) {
11331199 // We have a Server Component that specifies a key but we're now splitting
11341200 // the tree using a fragment.
@@ -1231,6 +1297,7 @@ function renderClientElement(
12311297 props : any ,
12321298 owner : null | ReactComponentInfo , // DEV-only
12331299 stack : null | string , // DEV-only
1300+ validated : number , // DEV-only
12341301) : ReactJSONValue {
12351302 // We prepend the terminal client element that actually gets serialized with
12361303 // the keys of any Server Components which are not serialized.
@@ -1242,7 +1309,7 @@ function renderClientElement(
12421309 }
12431310 const element = __DEV__
12441311 ? enableOwnerStacks
1245- ? [ REACT_ELEMENT_TYPE , type , key , props , owner , stack ]
1312+ ? [ REACT_ELEMENT_TYPE , type , key , props , owner , stack , validated ]
12461313 : [ REACT_ELEMENT_TYPE , type , key , props , owner ]
12471314 : [ REACT_ELEMENT_TYPE , type , key , props ] ;
12481315 if ( task . implicitSlot && key !== null ) {
@@ -1292,6 +1359,7 @@ function renderElement(
12921359 props : any ,
12931360 owner : null | ReactComponentInfo , // DEV only
12941361 stack : null | string , // DEV only
1362+ validated : number , // DEV only
12951363) : ReactJSONValue {
12961364 if ( ref !== null && ref !== undefined ) {
12971365 // When the ref moves to the regular props object this will implicitly
@@ -1312,7 +1380,15 @@ function renderElement(
13121380 if ( typeof type === 'function' ) {
13131381 if ( isClientReference ( type ) || isOpaqueTemporaryReference ( type ) ) {
13141382 // This is a reference to a Client Component.
1315- return renderClientElement ( task , type , key , props , owner , stack ) ;
1383+ return renderClientElement (
1384+ task ,
1385+ type ,
1386+ key ,
1387+ props ,
1388+ owner ,
1389+ stack ,
1390+ validated ,
1391+ ) ;
13161392 }
13171393 // This is a Server Component.
13181394 return renderFunctionComponent (
@@ -1323,10 +1399,11 @@ function renderElement(
13231399 props ,
13241400 owner ,
13251401 stack ,
1402+ validated ,
13261403 ) ;
13271404 } else if ( typeof type === 'string' ) {
13281405 // This is a host element. E.g. HTML.
1329- return renderClientElement ( task , type , key , props , owner , stack ) ;
1406+ return renderClientElement ( task , type , key , props , owner , stack , validated ) ;
13301407 } else if ( typeof type === 'symbol' ) {
13311408 if ( type === REACT_FRAGMENT_TYPE && key === null ) {
13321409 // For key-less fragments, we add a small optimization to avoid serializing
@@ -1347,11 +1424,19 @@ function renderElement(
13471424 }
13481425 // This might be a built-in React component. We'll let the client decide.
13491426 // Any built-in works as long as its props are serializable.
1350- return renderClientElement ( task , type , key , props , owner , stack ) ;
1427+ return renderClientElement ( task , type , key , props , owner , stack , validated ) ;
13511428 } else if ( type != null && typeof type === 'object' ) {
13521429 if ( isClientReference ( type ) ) {
13531430 // This is a reference to a Client Component.
1354- return renderClientElement ( task , type , key , props , owner , stack ) ;
1431+ return renderClientElement (
1432+ task ,
1433+ type ,
1434+ key ,
1435+ props ,
1436+ owner ,
1437+ stack ,
1438+ validated ,
1439+ ) ;
13551440 }
13561441 switch ( type . $$typeof ) {
13571442 case REACT_LAZY_TYPE : {
@@ -1372,6 +1457,7 @@ function renderElement(
13721457 props ,
13731458 owner ,
13741459 stack ,
1460+ validated ,
13751461 ) ;
13761462 }
13771463 case REACT_FORWARD_REF_TYPE : {
@@ -1383,6 +1469,7 @@ function renderElement(
13831469 props ,
13841470 owner ,
13851471 stack ,
1472+ validated ,
13861473 ) ;
13871474 }
13881475 case REACT_MEMO_TYPE : {
@@ -1395,6 +1482,7 @@ function renderElement(
13951482 props ,
13961483 owner ,
13971484 stack ,
1485+ validated ,
13981486 ) ;
13991487 }
14001488 }
@@ -1963,8 +2051,11 @@ function renderModelDestructive(
19632051 props ,
19642052 __DEV__ ? element . _owner : null ,
19652053 __DEV__ && enableOwnerStacks
1966- ? filterDebugStack ( element . _debugStack )
2054+ ? ! element . _debugStack || typeof element . _debugStack === 'string'
2055+ ? element . _debugStack
2056+ : filterDebugStack ( element . _debugStack )
19672057 : null ,
2058+ __DEV__ && enableOwnerStacks ? element . _store . validated : 0 ,
19682059 ) ;
19692060 }
19702061 case REACT_LAZY_TYPE : {
0 commit comments