Skip to content

Commit c49a32f

Browse files
committed
Unkeyed fragments needs a wrapper if the parent server component was keyed
1 parent 8e025ec commit c49a32f

File tree

1 file changed

+45
-3
lines changed

1 file changed

+45
-3
lines changed

packages/react-server/src/ReactFlightServer.js

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,48 @@ function createLazyWrapperAroundWakeable(wakeable: Wakeable) {
516516
return lazyType;
517517
}
518518

519+
function renderFragment(
520+
request: Request,
521+
task: Task,
522+
children: $ReadOnlyArray<ReactClientValue>,
523+
): ReactJSONValue {
524+
if (!enableServerComponentKeys) {
525+
return children;
526+
}
527+
if (task.keyPath !== null) {
528+
// We have a Server Component that specifies a key but we're now splitting
529+
// the tree using a fragment.
530+
const fragment = [
531+
REACT_ELEMENT_TYPE,
532+
REACT_FRAGMENT_TYPE,
533+
task.keyPath,
534+
{children},
535+
];
536+
if (!task.implicitSlot) {
537+
// If this was keyed inside a set. I.e. the outer Server Component was keyed
538+
// then we need to handle reorders of the whole set. To do this we need to wrap
539+
// this array in a keyed Fragment.
540+
return fragment;
541+
}
542+
// If the outer Server Component was implicit but then an inner one had a key
543+
// we don't actually need to be able to move the whole set around. It'll always be
544+
// in an implicit slot. The key only exists to be able to reset the state of the
545+
// children. We could achieve the same effect by passing on the keyPath to the next
546+
// set of components inside the fragment. This would also allow a keyless fragment
547+
// reconcile against a single child.
548+
// Unfortunately because of JSON.stringify, we can't call the recursive loop for
549+
// each child within this context because we can't return a set with already resolved
550+
// values. E.g. a string would get double encoded. Returning would pop the context.
551+
// So instead, we wrap it with an unkeyed fragment and inner keyed fragment.
552+
return [fragment];
553+
}
554+
// Since we're yielding here, that implicitly resets the keyPath context on the
555+
// way up. Which is what we want since we've consumed it. If this changes to
556+
// be recursive serialization, we need to reset the keyPath and implicitSlot,
557+
// before recursing here.
558+
return children;
559+
}
560+
519561
function renderClientElement(
520562
task: Task,
521563
type: any,
@@ -638,6 +680,7 @@ function renderElement(
638680
props.children,
639681
);
640682
task.implicitSlot = prevImplicitSlot;
683+
return json;
641684
}
642685
// This might be a built-in React component. We'll let the client decide.
643686
// Any built-in works as long as its props are serializable.
@@ -1334,8 +1377,7 @@ function renderModelDestructive(
13341377
}
13351378

13361379
if (isArray(value)) {
1337-
// $FlowFixMe[incompatible-return]
1338-
return value;
1380+
return renderFragment(request, task, value);
13391381
}
13401382

13411383
if (value instanceof Map) {
@@ -1401,7 +1443,7 @@ function renderModelDestructive(
14011443

14021444
const iteratorFn = getIteratorFn(value);
14031445
if (iteratorFn) {
1404-
return Array.from((value: any));
1446+
return renderFragment(request, task, Array.from((value: any)));
14051447
}
14061448

14071449
// Verify that this is a simple plain object.

0 commit comments

Comments
 (0)