@@ -16,6 +16,7 @@ import {
1616 enablePostpone ,
1717 enableTaint ,
1818 enableServerContext ,
19+ enableServerComponentKeys ,
1920} from 'shared/ReactFeatureFlags' ;
2021
2122import {
@@ -515,11 +516,44 @@ function createLazyWrapperAroundWakeable(wakeable: Wakeable) {
515516 return lazyType ;
516517}
517518
519+ function renderClientElement(
520+ task: Task,
521+ type: any,
522+ key: null | string,
523+ props: any,
524+ ): ReactJSONValue {
525+ if ( ! enableServerComponentKeys ) {
526+ return [ REACT_ELEMENT_TYPE , type , key , props ] ;
527+ }
528+ // We prepend the terminal client element that actually gets serialized with
529+ // the keys of any Server Components which are not serialized.
530+ const keyPath = task.keyPath;
531+ if (key === null) {
532+ key = keyPath ;
533+ } else if (keyPath !== null) {
534+ key = keyPath + ',' + key ;
535+ }
536+ const element = [REACT_ELEMENT_TYPE, type, key, props];
537+ if (task.implicitSlot && key !== null ) {
538+ // The root Server Component had no key so it was in an implicit slot.
539+ // If we had a key lower, it would end up in that slot with an explicit key.
540+ // We wrap the element in a fragment to give it an implicit key slot with
541+ // an inner explicit key.
542+ return [ element ] ;
543+ }
544+ // Since we're yielding here, that implicitly resets the keyPath context on the
545+ // way up. Which is what we want since we've consumed it. If this changes to
546+ // be recursive serialization, we need to reset the keyPath and implicitSlot,
547+ // before recursing here. We also need to reset it once we render into an array
548+ // or anything else too which we also get implicitly.
549+ return element;
550+ }
551+
518552function renderElement (
519553 request : Request ,
520554 task : Task ,
521555 type : any ,
522- key: null | React$Key ,
556+ key : null | string ,
523557 ref : mixed ,
524558 props : any ,
525559) : ReactJSONValue {
@@ -540,7 +574,7 @@ function renderElement(
540574 if ( typeof type === 'function ') {
541575 if ( isClientReference ( type ) ) {
542576 // This is a reference to a Client Component.
543- return [ REACT_ELEMENT_TYPE , type , key , props ] ;
577+ return renderClientElement ( task , type , key , props ) ;
544578 }
545579 // This is a server-side component.
546580
@@ -567,31 +601,51 @@ function renderElement(
567601 // the thenable here.
568602 result = createLazyWrapperAroundWakeable ( result ) ;
569603 }
570- return renderModelDestructive(request, task, emptyRoot, '', result);
604+ // Track this element's key on the Server Component on the keyPath context..
605+ const prevKeyPath = task.keyPath;
606+ const prevImplicitSlot = task.implicitSlot;
607+ if (key !== null) {
608+ // Append the key to the path. Technically a null key should really add the child
609+ // index. We don't do that to hold the payload small and implementation simple.
610+ task . keyPath = prevKeyPath === null ? key : prevKeyPath + ',' + key ;
611+ } else if (prevKeyPath === null) {
612+ // This sequence of Server Components has no keys. This means that it was rendered
613+ // in a slot that needs to assign an implicit key. Even if children below have
614+ // explicit keys, they should not be used for the outer most key since it might
615+ // collide with other slots in that set.
616+ task . implicitSlot = true ;
617+ }
618+ const json = renderModelDestructive(request, task, emptyRoot, '', result);
619+ task.keyPath = prevKeyPath;
620+ task.implicitSlot = prevImplicitSlot;
621+ return json;
571622 } else if ( typeof type === 'string ') {
572623 // This is a host element. E.g. HTML.
573- return [ REACT_ELEMENT_TYPE , type , key , props ] ;
624+ return renderClientElement ( task , type , key , props ) ;
574625 } else if (typeof type === 'symbol') {
575- if ( type === REACT_FRAGMENT_TYPE ) {
626+ if ( type === REACT_FRAGMENT_TYPE && key === null ) {
576627 // For key-less fragments, we add a small optimization to avoid serializing
577628 // it as a wrapper.
578- // TODO: If a key is specified, we should propagate its key to any children.
579- // Same as if a Server Component has a key.
580- return renderModelDestructive (
629+ const prevImplicitSlot = task . implicitSlot ;
630+ if ( task . keyPath === null ) {
631+ task . implicitSlot = true ;
632+ }
633+ const json = renderModelDestructive(
581634 request,
582635 task,
583636 emptyRoot,
584637 '',
585638 props.children,
586639 );
640+ task.implicitSlot = prevImplicitSlot;
587641 }
588642 // This might be a built-in React component. We'll let the client decide.
589643 // Any built-in works as long as its props are serializable.
590- return [REACT_ELEMENT_TYPE , type, key, props] ;
644+ return renderClientElement ( task , type , key , props ) ;
591645 } else if ( type != null && typeof type === 'object ') {
592646 if ( isClientReference ( type ) ) {
593647 // This is a reference to a Client Component.
594- return [ REACT_ELEMENT_TYPE , type , key , props ] ;
648+ return renderClientElement ( task , type , key , props ) ;
595649 }
596650 switch (type.$$typeof) {
597651 case REACT_LAZY_TYPE : {
@@ -611,7 +665,29 @@ function renderElement(
611665
612666 prepareToUseHooksForComponent ( prevThenableState ) ;
613667 const result = render ( props , undefined ) ;
614- return renderModelDestructive ( request , task , emptyRoot , '' , result ) ;
668+ const prevKeyPath = task . keyPath ;
669+ const prevImplicitSlot = task . implicitSlot ;
670+ if ( key !== null ) {
671+ // Append the key to the path. Technically a null key should really add the child
672+ // index. We don't do that to hold the payload small and implementation simple.
673+ task. keyPath = prevKeyPath === null ? key : prevKeyPath + ',' + key ;
674+ } else if (prevKeyPath === null) {
675+ // This sequence of Server Components has no keys. This means that it was rendered
676+ // in a slot that needs to assign an implicit key. Even if children below have
677+ // explicit keys, they should not be used for the outer most key since it might
678+ // collide with other slots in that set.
679+ task . implicitSlot = true ;
680+ }
681+ const json = renderModelDestructive(
682+ request,
683+ task,
684+ emptyRoot,
685+ '',
686+ result,
687+ );
688+ task.keyPath = prevKeyPath;
689+ task.implicitSlot = prevImplicitSlot;
690+ return json;
615691 }
616692 case REACT_MEMO_TYPE : {
617693 return renderElement ( request , task , type . type , key , ref , props ) ;
@@ -633,13 +709,13 @@ function renderElement(
633709 ) ;
634710 }
635711 }
636- return [
637- REACT_ELEMENT_TYPE ,
712+ return renderClientElement (
713+ task ,
638714 type ,
639715 key ,
640716 // Rely on __popProvider being serialized last to pop the provider.
641717 { value : props . value , children : props . children , __pop : POP } ,
642- ] ;
718+ ) ;
643719 }
644720 // Fallthrough
645721 }
@@ -1009,6 +1085,8 @@ function renderModel(
10091085 key : string ,
10101086 value : ReactClientValue ,
10111087) : ReactJSONValue {
1088+ const prevKeyPath = task . keyPath ;
1089+ const prevImplicitSlot = task . implicitSlot ;
10121090 try {
10131091 return renderModelDestructive ( request , task , parent , key , value ) ;
10141092 } catch ( thrownValue ) {
@@ -1045,6 +1123,12 @@ function renderModel(
10451123 const ping = newTask . ping ;
10461124 ( x : any ) . then ( ping , ping ) ;
10471125 newTask . thenableState = getThenableStateAfterSuspending ( ) ;
1126+
1127+ // Restore the context. We assume that this will be restored by the inner
1128+ // functions in case nothing throws so we don't use "finally" here.
1129+ task . keyPath = prevKeyPath ;
1130+ task . implicitSlot = prevImplicitSlot ;
1131+
10481132 if ( wasReactNode ) {
10491133 return serializeLazyID ( newTask . id ) ;
10501134 }
@@ -1057,12 +1141,24 @@ function renderModel(
10571141 const postponeId = request . nextChunkId ++ ;
10581142 logPostpone ( request , postponeInstance . message ) ;
10591143 emitPostponeChunk ( request , postponeId , postponeInstance ) ;
1144+
1145+ // Restore the context. We assume that this will be restored by the inner
1146+ // functions in case nothing throws so we don't use "finally" here.
1147+ task . keyPath = prevKeyPath ;
1148+ task . implicitSlot = prevImplicitSlot ;
1149+
10601150 if ( wasReactNode ) {
10611151 return serializeLazyID ( postponeId ) ;
10621152 }
10631153 return serializeByValueID ( postponeId ) ;
10641154 }
10651155 }
1156+
1157+ // Restore the context. We assume that this will be restored by the inner
1158+ // functions in case nothing throws so we don't use "finally" here.
1159+ task . keyPath = prevKeyPath ;
1160+ task . implicitSlot = prevImplicitSlot ;
1161+
10661162 if ( wasReactNode ) {
10671163 // Something errored. We'll still send everything we have up until this point.
10681164 // We'll replace this element with a lazy reference that throws on the client
@@ -1131,13 +1227,13 @@ function renderModelDestructive(
11311227 writtenObjects . set ( value , - 1 ) ;
11321228 }
11331229
1134- // TODO: Concatenate keys of parents onto children.
11351230 const element : React$Element < any > = ( value : any ) ;
11361231 // Attempt to render the Server Component.
11371232 return renderElement (
11381233 request ,
11391234 task ,
11401235 element . type ,
1236+ // $FlowFixMe[incompatible-call] the key of an element is null | string
11411237 element . key ,
11421238 element . ref ,
11431239 element . props ,
@@ -1626,6 +1722,10 @@ function retryTask(request: Request, task: Task): void {
16261722 // Track the root again for the resolved object.
16271723 modelRoot = resolvedModel ;
16281724
1725+ // The keyPath resets at any terminal child node.
1726+ task . keyPath = null ;
1727+ task . implicitSlot = false ;
1728+
16291729 // If the value is a string, it means it's a terminal value adn we already escaped it
16301730 // We don't need to escape it again so it's not passed the toJSON replacer.
16311731 // Object might contain unresolved values like additional elements.
0 commit comments