@@ -337,6 +337,8 @@ export opaque type Request = {
337
337
onPostpone : ( reason : string , postponeInfo : ThrownInfo ) => void ,
338
338
// Form state that was the result of an MPA submission, if it was provided.
339
339
formState : null | ReactFormState < any , any> ,
340
+ // DEV-only, warning dedupe
341
+ didWarnForKey ?: null | WeakSet < ComponentStackNode > ,
340
342
} ;
341
343
342
344
// This is a default heuristic for how to split up the HTML content into progressive
@@ -409,6 +411,9 @@ export function createRequest(
409
411
onFatalError : onFatalError === undefined ? noop : onFatalError ,
410
412
formState : formState === undefined ? null : formState ,
411
413
} ;
414
+ if ( __DEV__ ) {
415
+ request . didWarnForKey = null ;
416
+ }
412
417
// This segment represents the root fallback.
413
418
const rootSegment = createPendingSegment (
414
419
request ,
@@ -787,6 +792,19 @@ function createClassComponentStack(
787
792
} ;
788
793
}
789
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
+
790
808
type ThrownInfo = {
791
809
componentStack ?: string ,
792
810
} ;
@@ -2597,6 +2615,59 @@ function replayFragment(
2597
2615
}
2598
2616
}
2599
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
+
2600
2671
function renderChildrenArray (
2601
2672
request : Request ,
2602
2673
task : Task ,
@@ -2618,6 +2689,7 @@ function renderChildrenArray(
2618
2689
return ;
2619
2690
}
2620
2691
}
2692
+
2621
2693
const prevTreeContext = task . treeContext ;
2622
2694
const totalChildren = children . length ;
2623
2695
@@ -2650,6 +2722,9 @@ function renderChildrenArray(
2650
2722
2651
2723
for ( let i = 0 ; i < totalChildren ; i ++ ) {
2652
2724
const node = children [ i ] ;
2725
+ if ( __DEV__ ) {
2726
+ warnForMissingKey ( request , task , node ) ;
2727
+ }
2653
2728
task . treeContext = pushTreeContext ( prevTreeContext , totalChildren , i ) ;
2654
2729
// We need to use the non-destructive form so that we can safely pop back
2655
2730
// up and render the sibling if something suspends.
0 commit comments