Skip to content

Commit 8e025ec

Browse files
committed
Pass the keyPath context when rendering a Server Component
This is then prepended to the key of the terminal client element.
1 parent b729be0 commit 8e025ec

File tree

1 file changed

+115
-15
lines changed

1 file changed

+115
-15
lines changed

packages/react-server/src/ReactFlightServer.js

Lines changed: 115 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
enablePostpone,
1717
enableTaint,
1818
enableServerContext,
19+
enableServerComponentKeys,
1920
} from 'shared/ReactFeatureFlags';
2021

2122
import {
@@ -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+
518552
function 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

Comments
 (0)