@@ -151,6 +151,13 @@ const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
151151const ReactCurrentCache = ReactSharedInternals . ReactCurrentCache ;
152152const ReactDebugCurrentFrame = ReactSharedInternals . ReactDebugCurrentFrame ;
153153
154+ // Linked list representing the identity of a component given the component/tag name and key.
155+ // The name might be minified but we assume that it's going to be the same generated name. Typically
156+ // because it's just the same compiled output in practice.
157+ type KeyNode =
158+ | null
159+ | [ KeyNode /* parent */ , string | null /* name */ , string | number /* key */ ] ;
160+
154161type LegacyContext = {
155162 [ key : string ] : any ,
156163} ;
@@ -176,6 +183,7 @@ export type Task = {
176183 blockedBoundary : Root | SuspenseBoundary ,
177184 blockedSegment : Segment , // the segment we'll write to
178185 abortSet : Set < Task > , // the abortable set that this task belongs to
186+ keyPath : KeyNode , // the path of all parent keys currently rendering
179187 legacyContext : LegacyContext , // the current legacy context that this task is executing in
180188 context : ContextSnapshot , // the current new context that this task is executing in
181189 treeContext : TreeContext , // the current tree context that this task is executing in
@@ -335,6 +343,7 @@ export function createRequest(
335343 null ,
336344 rootSegment ,
337345 abortSet ,
346+ null ,
338347 emptyContextObject ,
339348 rootContextSnapshot ,
340349 emptyTreeContext ,
@@ -388,6 +397,7 @@ function createTask(
388397 blockedBoundary : Root | SuspenseBoundary ,
389398 blockedSegment : Segment ,
390399 abortSet : Set < Task > ,
400+ keyPath : KeyNode ,
391401 legacyContext : LegacyContext ,
392402 context : ContextSnapshot ,
393403 treeContext : TreeContext ,
@@ -404,6 +414,7 @@ function createTask(
404414 blockedBoundary,
405415 blockedSegment,
406416 abortSet,
417+ keyPath,
407418 legacyContext,
408419 context,
409420 treeContext,
@@ -617,7 +628,7 @@ function renderSuspenseBoundary(
617628 }
618629 try {
619630 // We use the safe form because we don't handle suspending here. Only error handling.
620- renderNode ( request , task , content ) ;
631+ renderNode ( request , task , content , 0 ) ;
621632 pushSegmentFinale (
622633 contentRootSegment . chunks ,
623634 request . responseState ,
@@ -678,6 +689,7 @@ function renderSuspenseBoundary(
678689 parentBoundary ,
679690 boundarySegment ,
680691 fallbackAbortSet ,
692+ task . keyPath ,
681693 task . legacyContext ,
682694 task . context ,
683695 task . treeContext ,
@@ -703,7 +715,7 @@ function renderBackupSuspenseBoundary(
703715 const segment = task . blockedSegment ;
704716
705717 pushStartCompletedSuspenseBoundary ( segment . chunks ) ;
706- renderNode ( request , task , content ) ;
718+ renderNode ( request , task , content , 0 ) ;
707719 pushEndCompletedSuspenseBoundary ( segment . chunks ) ;
708720
709721 popComponentStackInDEV ( task ) ;
@@ -733,7 +745,7 @@ function renderHostElement(
733745
734746 // We use the non-destructive form because if something suspends, we still
735747 // need to pop back up and finish this subtree of HTML.
736- renderNode ( request , task , children ) ;
748+ renderNode ( request , task , children , 0 ) ;
737749
738750 // We expect that errors will fatal the whole task and that we don't need
739751 // the correct context. Therefore this is not in a finally.
@@ -800,13 +812,13 @@ function finishClassComponent(
800812 childContextTypes ,
801813 ) ;
802814 task . legacyContext = mergedContext ;
803- renderNodeDestructive ( request , task , null , nextChildren ) ;
815+ renderNodeDestructive ( request , task , null , nextChildren , 0 ) ;
804816 task . legacyContext = previousContext ;
805817 return ;
806818 }
807819 }
808820
809- renderNodeDestructive ( request , task , null , nextChildren ) ;
821+ renderNodeDestructive ( request , task , null , nextChildren , 0 ) ;
810822}
811823
812824function renderClassComponent (
@@ -957,12 +969,12 @@ function renderIndeterminateComponent(
957969 const index = 0 ;
958970 task . treeContext = pushTreeContext ( prevTreeContext , totalChildren , index ) ;
959971 try {
960- renderNodeDestructive ( request , task , null , value ) ;
972+ renderNodeDestructive ( request , task , null , value , 0 ) ;
961973 } finally {
962974 task . treeContext = prevTreeContext ;
963975 }
964976 } else {
965- renderNodeDestructive ( request , task , null , value ) ;
977+ renderNodeDestructive ( request , task , null , value , 0 ) ;
966978 }
967979 }
968980 popComponentStackInDEV ( task ) ;
@@ -1062,12 +1074,12 @@ function renderForwardRef(
10621074 const index = 0 ;
10631075 task . treeContext = pushTreeContext ( prevTreeContext , totalChildren , index ) ;
10641076 try {
1065- renderNodeDestructive ( request , task , null , children ) ;
1077+ renderNodeDestructive ( request , task , null , children , 0 ) ;
10661078 } finally {
10671079 task . treeContext = prevTreeContext ;
10681080 }
10691081 } else {
1070- renderNodeDestructive ( request , task , null , children ) ;
1082+ renderNodeDestructive ( request , task , null , children , 0 ) ;
10711083 }
10721084 popComponentStackInDEV ( task ) ;
10731085}
@@ -1139,7 +1151,7 @@ function renderContextConsumer(
11391151 const newValue = readContext ( context ) ;
11401152 const newChildren = render ( newValue ) ;
11411153
1142- renderNodeDestructive ( request , task , null , newChildren ) ;
1154+ renderNodeDestructive ( request , task , null , newChildren , 0 ) ;
11431155}
11441156
11451157function renderContextProvider (
@@ -1156,7 +1168,7 @@ function renderContextProvider(
11561168 prevSnapshot = task . context ;
11571169 }
11581170 task . context = pushProvider ( context , value ) ;
1159- renderNodeDestructive ( request , task , null , children ) ;
1171+ renderNodeDestructive ( request , task , null , children , 0 ) ;
11601172 task . context = popProvider ( context ) ;
11611173 if ( __DEV__ ) {
11621174 if ( prevSnapshot !== task . context ) {
@@ -1199,7 +1211,7 @@ function renderOffscreen(request: Request, task: Task, props: Object): void {
11991211 } else {
12001212 // A visible Offscreen boundary is treated exactly like a fragment: a
12011213 // pure indirection.
1202- renderNodeDestructive ( request , task , null , props . children ) ;
1214+ renderNodeDestructive ( request , task , null , props . children , 0 ) ;
12031215 }
12041216}
12051217
@@ -1246,7 +1258,7 @@ function renderElement(
12461258 case REACT_STRICT_MODE_TYPE :
12471259 case REACT_PROFILER_TYPE :
12481260 case REACT_FRAGMENT_TYPE : {
1249- renderNodeDestructive ( request , task , null , props . children ) ;
1261+ renderNodeDestructive ( request , task , null , props . children , 0 ) ;
12501262 return ;
12511263 }
12521264 case REACT_OFFSCREEN_TYPE : {
@@ -1256,13 +1268,13 @@ function renderElement(
12561268 case REACT_SUSPENSE_LIST_TYPE : {
12571269 pushBuiltInComponentStackInDEV ( task , 'SuspenseList' ) ;
12581270 // TODO: SuspenseList should control the boundaries.
1259- renderNodeDestructive ( request , task , null , props . children ) ;
1271+ renderNodeDestructive ( request , task , null , props . children , 0 ) ;
12601272 popComponentStackInDEV ( task ) ;
12611273 return ;
12621274 }
12631275 case REACT_SCOPE_TYPE : {
12641276 if ( enableScopeAPI ) {
1265- renderNodeDestructive ( request , task , null , props . children ) ;
1277+ renderNodeDestructive ( request , task , null , props . children , 0 ) ;
12661278 return ;
12671279 }
12681280 throw new Error ( 'ReactDOMServer does not yet support scope components.' ) ;
@@ -1368,13 +1380,20 @@ function renderNodeDestructive(
13681380 // always null, except when called by retryTask.
13691381 prevThenableState : ThenableState | null ,
13701382 node : ReactNodeList ,
1383+ childIndex : number ,
13711384) : void {
13721385 if ( __DEV__ ) {
13731386 // In Dev we wrap renderNodeDestructiveImpl in a try / catch so we can capture
13741387 // a component stack at the right place in the tree. We don't do this in renderNode
13751388 // becuase it is not called at every layer of the tree and we may lose frames
13761389 try {
1377- return renderNodeDestructiveImpl ( request , task , prevThenableState , node ) ;
1390+ return renderNodeDestructiveImpl (
1391+ request ,
1392+ task ,
1393+ prevThenableState ,
1394+ node ,
1395+ childIndex ,
1396+ ) ;
13781397 } catch ( x ) {
13791398 if ( typeof x === 'object' && x !== null && typeof x . then === 'function' ) {
13801399 // This is a Wakable, noop
@@ -1389,7 +1408,13 @@ function renderNodeDestructive(
13891408 throw x ;
13901409 }
13911410 } else {
1392- return renderNodeDestructiveImpl ( request , task , prevThenableState , node ) ;
1411+ return renderNodeDestructiveImpl (
1412+ request ,
1413+ task ,
1414+ prevThenableState ,
1415+ node ,
1416+ childIndex ,
1417+ ) ;
13931418 }
13941419}
13951420
@@ -1400,6 +1425,7 @@ function renderNodeDestructiveImpl(
14001425 task : Task ,
14011426 prevThenableState : ThenableState | null ,
14021427 node : ReactNodeList ,
1428+ childIndex : number ,
14031429) : void {
14041430 // Stash the node we're working on. We'll pick up from this task in case
14051431 // something suspends.
@@ -1411,9 +1437,14 @@ function renderNodeDestructiveImpl(
14111437 case REACT_ELEMENT_TYPE : {
14121438 const element : React$Element < any > = ( node : any ) ;
14131439 const type = element . type ;
1440+ const key = element . key ;
14141441 const props = element . props ;
14151442 const ref = element . ref ;
1443+ const name = getComponentNameFromType ( type ) ;
1444+ const prevKeyPath = task . keyPath ;
1445+ task . keyPath = [ task . keyPath , name , key == null ? childIndex : key ] ;
14161446 renderElement ( request , task , prevThenableState , type , props , ref ) ;
1447+ task . keyPath = prevKeyPath ;
14171448 return ;
14181449 }
14191450 case REACT_PORTAL_TYPE:
@@ -1446,13 +1477,13 @@ function renderNodeDestructiveImpl(
14461477 } else {
14471478 resolvedNode = init ( payload ) ;
14481479 }
1449- renderNodeDestructive ( request , task , null , resolvedNode ) ;
1480+ renderNodeDestructive ( request , task , null , resolvedNode , childIndex ) ;
14501481 return ;
14511482 }
14521483 }
14531484
14541485 if ( isArray ( node ) ) {
1455- renderChildrenArray ( request , task , node ) ;
1486+ renderChildrenArray ( request , task , node , childIndex ) ;
14561487 return ;
14571488 }
14581489
@@ -1476,7 +1507,7 @@ function renderNodeDestructiveImpl(
14761507 children . push ( step . value ) ;
14771508 step = iterator . next ( ) ;
14781509 } while ( ! step . done ) ;
1479- renderChildrenArray ( request , task , children ) ;
1510+ renderChildrenArray ( request , task , children , childIndex ) ;
14801511 return ;
14811512 }
14821513 return ;
@@ -1500,6 +1531,7 @@ function renderNodeDestructiveImpl(
15001531 task ,
15011532 null ,
15021533 unwrapThenable ( thenable ) ,
1534+ childIndex ,
15031535 ) ;
15041536 }
15051537
@@ -1513,6 +1545,7 @@ function renderNodeDestructiveImpl(
15131545 task ,
15141546 null ,
15151547 readContext ( context ) ,
1548+ childIndex ,
15161549 ) ;
15171550 }
15181551
@@ -1567,17 +1600,26 @@ function renderChildrenArray(
15671600 request : Request ,
15681601 task : Task ,
15691602 children : Array < any > ,
1603+ childIndex : number ,
15701604) {
1605+ const prevKeyPath = task . keyPath ;
15711606 const totalChildren = children . length ;
15721607 for ( let i = 0 ; i < totalChildren ; i ++ ) {
15731608 const prevTreeContext = task . treeContext ;
15741609 task . treeContext = pushTreeContext ( prevTreeContext , totalChildren , i ) ;
15751610 try {
1611+ const node = children [ i ] ;
1612+ if ( isArray ( node ) || getIteratorFn ( node ) ) {
1613+ // Nested arrays behave like a "fragment node" which is keyed.
1614+ // Therefore we need to add the current index as a parent key.
1615+ task . keyPath = [ task . keyPath , '' , childIndex ] ;
1616+ }
15761617 // We need to use the non-destructive form so that we can safely pop back
15771618 // up and render the sibling if something suspends.
1578- renderNode ( request , task , children [ i ] ) ;
1619+ renderNode ( request , task , node , i ) ;
15791620 } finally {
15801621 task . treeContext = prevTreeContext ;
1622+ task . keyPath = prevKeyPath ;
15811623 }
15821624 }
15831625}
@@ -1611,6 +1653,7 @@ function spawnNewSuspendedTask(
16111653 task . blockedBoundary ,
16121654 newSegment ,
16131655 task . abortSet ,
1656+ task . keyPath ,
16141657 task . legacyContext ,
16151658 task . context ,
16161659 task . treeContext ,
@@ -1629,7 +1672,12 @@ function spawnNewSuspendedTask(
16291672
16301673// This is a non-destructive form of rendering a node. If it suspends it spawns
16311674// a new task and restores the context of this task to what it was before.
1632- function renderNode ( request : Request , task : Task , node : ReactNodeList ) : void {
1675+ function renderNode (
1676+ request : Request ,
1677+ task : Task ,
1678+ node : ReactNodeList ,
1679+ childIndex : number ,
1680+ ) : void {
16331681 // Store how much we've pushed at this point so we can reset it in case something
16341682 // suspended partially through writing something.
16351683 const segment = task . blockedSegment ;
@@ -1641,12 +1689,13 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
16411689 const previousFormatContext = task . blockedSegment . formatContext ;
16421690 const previousLegacyContext = task . legacyContext ;
16431691 const previousContext = task . context ;
1692+ const previousKeyPath = task . keyPath ;
16441693 let previousComponentStack = null ;
16451694 if ( __DEV__ ) {
16461695 previousComponentStack = task . componentStack ;
16471696 }
16481697 try {
1649- return renderNodeDestructive ( request , task , null , node ) ;
1698+ return renderNodeDestructive ( request , task , null , node , childIndex ) ;
16501699 } catch ( thrownValue ) {
16511700 resetHooksState ( ) ;
16521701
@@ -1675,6 +1724,7 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
16751724 task . blockedSegment . formatContext = previousFormatContext ;
16761725 task . legacyContext = previousLegacyContext ;
16771726 task . context = previousContext ;
1727+ task . keyPath = previousKeyPath ;
16781728 // Restore all active ReactContexts to what they were before.
16791729 switchContext ( previousContext ) ;
16801730 if ( __DEV__ ) {
@@ -1687,6 +1737,7 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
16871737 task . blockedSegment . formatContext = previousFormatContext ;
16881738 task . legacyContext = previousLegacyContext ;
16891739 task . context = previousContext ;
1740+ task . keyPath = previousKeyPath ;
16901741 // Restore all active ReactContexts to what they were before.
16911742 switchContext ( previousContext ) ;
16921743 if ( __DEV__ ) {
@@ -1955,7 +2006,7 @@ function retryTask(request: Request, task: Task): void {
19552006 const prevThenableState = task . thenableState ;
19562007 task . thenableState = null ;
19572008
1958- renderNodeDestructive ( request , task , prevThenableState , task . node ) ;
2009+ renderNodeDestructive ( request , task , prevThenableState , task . node , 0 ) ;
19592010 pushSegmentFinale (
19602011 segment . chunks ,
19612012 request . responseState ,
0 commit comments