16
enablePostpone,
17
enableTaint,
18
enableServerContext,
19
+ enableServerComponentKeys,
20
} from 'shared/ReactFeatureFlags';
21
22
import {
182
model: ReactClientValue,
183
ping: () => void,
184
toJSON: (key: string, value: ReactClientValue) => ReactJSONValue,
185
+ keyPath: null | string, // parent server component keys
186
+ implicitSlot: boolean, // true if the root server component of this sequence had a null key
187
context: ContextSnapshot,
188
thenableState: ThenableState | null,
189
};
317
};
318
request.pendingChunks++;
319
const rootContext = createRootContext(context);
317
- const rootTask = createTask(request, model, rootContext, abortSet);
320
+ const rootTask = createTask(
321
+ request,
322
+ model,
323
+ null,
324
+ false,
325
+ rootContext,
326
+ abortSet,
327
+ );
328
pingedTasks.push(rootTask);
329
return request;
330
}
348
349
const POP = {};
350
341
-function serializeThenable(request: Request, thenable: Thenable<any>): number {
351
+function serializeThenable(
352
+ request: Request,
353
+ task: Task,
354
+ thenable: Thenable<any>,
355
+): number {
356
request.pendingChunks++;
357
const newTask = createTask(
358
request,
359
null,
346
- getActiveContext(),
360
+ task.keyPath, // the server component sequence continues through Promise-as-a-child.
361
+ task.implicitSlot,
362
+ task.context,
363
request.abortableTasks,
364
);
365
516
return lazyType;
517
}
518
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
+
561
+function renderClientElement(
562
+ task: Task,
563
+ type: any,
564
+ key: null | string,
565
+ props: any,
566
+): ReactJSONValue {
567
+ if (!enableServerComponentKeys) {
568
+ return [REACT_ELEMENT_TYPE, type, key, props];
569
+ }
570
+ // We prepend the terminal client element that actually gets serialized with
571
+ // the keys of any Server Components which are not serialized.
572
+ const keyPath = task.keyPath;
573
+ if (key === null) {
574
+ key = keyPath;
575
+ } else if (keyPath !== null) {
576
+ key = keyPath + ',' + key;
577
+ }
578
+ const element = [REACT_ELEMENT_TYPE, type, key, props];
579
+ if (task.implicitSlot && key !== null) {
580
+ // The root Server Component had no key so it was in an implicit slot.
581
+ // If we had a key lower, it would end up in that slot with an explicit key.
582
+ // We wrap the element in a fragment to give it an implicit key slot with
583
+ // an inner explicit key.
584
+ return [element];
585
+ }
586
+ // Since we're yielding here, that implicitly resets the keyPath context on the
587
+ // way up. Which is what we want since we've consumed it. If this changes to
588
+ // be recursive serialization, we need to reset the keyPath and implicitSlot,
589
+ // before recursing here. We also need to reset it once we render into an array
590
+ // or anything else too which we also get implicitly.
591
+ return element;
592
+}
593
+
594
function renderElement(
595
request: Request,
596
task: Task,
597
type: any,
507
- key: null | React$Key,
598
+ key: null | string,
599
ref: mixed,
600
props: any,
601
): ReactJSONValue {
616
if (typeof type === 'function') {
617
if (isClientReference(type)) {
618
// This is a reference to a Client Component.
528
- return [REACT_ELEMENT_TYPE, type, key, props];
619
+ return renderClientElement(task, type, key, props);
620
}
621
// This is a server-side component.
622
643
// the thenable here.
644
result = createLazyWrapperAroundWakeable(result);
645
}
555
- return renderModelDestructive(request, task, emptyRoot, '', result);
646
+ // Track this element's key on the Server Component on the keyPath context..
647
+ const prevKeyPath = task.keyPath;
648
+ const prevImplicitSlot = task.implicitSlot;
649
+ if (key !== null) {
650
+ // Append the key to the path. Technically a null key should really add the child
651
+ // index. We don't do that to hold the payload small and implementation simple.
652
+ task.keyPath = prevKeyPath === null ? key : prevKeyPath + ',' + key;
653
+ } else if (prevKeyPath === null) {
654
+ // This sequence of Server Components has no keys. This means that it was rendered
655
+ // in a slot that needs to assign an implicit key. Even if children below have
656
+ // explicit keys, they should not be used for the outer most key since it might
657
+ // collide with other slots in that set.
658
+ task.implicitSlot = true;
659
+ }
660
+ const json = renderModelDestructive(request, task, emptyRoot, '', result);
661
+ task.keyPath = prevKeyPath;
662
+ task.implicitSlot = prevImplicitSlot;
663
+ return json;
664
} else if (typeof type === 'string') {
665
// This is a host element. E.g. HTML.
558
- return [REACT_ELEMENT_TYPE, type, key, props];
666
+ return renderClientElement(task, type, key, props);
667
} else if (typeof type === 'symbol') {
560
- if (type === REACT_FRAGMENT_TYPE) {
668
+ if (type === REACT_FRAGMENT_TYPE && key === null) {
669
// For key-less fragments, we add a small optimization to avoid serializing
670
// it as a wrapper.
563
- // TODO: If a key is specified, we should propagate its key to any children.
564
- // Same as if a Server Component has a key.
565
- return renderModelDestructive(
671
+ const prevImplicitSlot = task.implicitSlot;
672
+ if (task.keyPath === null) {
673
+ task.implicitSlot = true;
674
+ }
675
+ const json = renderModelDestructive(
676
request,
677
task,
678
emptyRoot,
679
'',
680
props.children,
681
);
682
+ task.implicitSlot = prevImplicitSlot;
683
+ return json;
684
}
685
// This might be a built-in React component. We'll let the client decide.
686
// Any built-in works as long as its props are serializable.
575
- return [REACT_ELEMENT_TYPE, type, key, props];
687
+ return renderClientElement(task, type, key, props);
688
} else if (type != null && typeof type === 'object') {
689
if (isClientReference(type)) {
690
// This is a reference to a Client Component.
579
- return [REACT_ELEMENT_TYPE, type, key, props];
691
+ return renderClientElement(task, type, key, props);
692
}
693
switch (type.$$typeof) {
694
case REACT_LAZY_TYPE: {
708
709
prepareToUseHooksForComponent(prevThenableState);
710
const result = render(props, undefined);
599
- return renderModelDestructive(request, task, emptyRoot, '', result);
711
+ const prevKeyPath = task.keyPath;
712
+ const prevImplicitSlot = task.implicitSlot;
713
+ if (key !== null) {
714
+ // Append the key to the path. Technically a null key should really add the child
715
+ // index. We don't do that to hold the payload small and implementation simple.
716
+ task.keyPath = prevKeyPath === null ? key : prevKeyPath + ',' + key;
717
+ } else if (prevKeyPath === null) {
718
+ // This sequence of Server Components has no keys. This means that it was rendered
719
+ // in a slot that needs to assign an implicit key. Even if children below have
720
+ // explicit keys, they should not be used for the outer most key since it might
721
+ // collide with other slots in that set.
722
+ task.implicitSlot = true;
723
+ }
724
+ const json = renderModelDestructive(
725
+ request,
726
+ task,
727
+ emptyRoot,
728
+ '',
729
+ result,
730
+ );
731
+ task.keyPath = prevKeyPath;
732
+ task.implicitSlot = prevImplicitSlot;
733
+ return json;
734
}
735
case REACT_MEMO_TYPE: {
736
return renderElement(request, task, type.type, key, ref, props);
752
);
753
}
754
}
621
- return [
622
- REACT_ELEMENT_TYPE,
755
+ return renderClientElement(
756
+ task,
757
type,
758
key,
759
// Rely on __popProvider being serialized last to pop the provider.
760
{value: props.value, children: props.children, __pop: POP},
627
- ];
761
+ );
762
}
763
// Fallthrough
764
}
781
function createTask(
782
request: Request,
783
model: ReactClientValue,
784
+ keyPath: null | string,
785
+ implicitSlot: boolean,
786
context: ContextSnapshot,
787
abortSet: Set<Task>,
788
): Task {
789
const id = request.nextChunkId++;
790
if (typeof model === 'object' && model !== null) {
655
- // Register this model as having the ID we're about to write.
656
- request.writtenObjects.set(model, id);
791
+ // If we're about to write this into a new task we can assign it an ID early so that
792
+ // any other references can refer to the value we're about to write.
793
+ if (
794
+ enableServerComponentKeys &&
795
+ (keyPath !== null || implicitSlot || context !== rootContextSnapshot)
796
+ ) {
797
+ // If we're in some kind of context we can't necessarily reuse this object depending
798
+ // what parent components are used.
799
+ } else {
800
+ request.writtenObjects.set(model, id);
801
+ }
802
}
803
const task: Task = {
804
id,
805
status: PENDING,
806
model,
807
+ keyPath,
808
+ implicitSlot,
809
context,
810
ping: () => pingTask(request, task),
811
toJSON: function (
1002
const newTask = createTask(
1003
request,
1004
value,
858
- getActiveContext(),
1005
+ null, // The way we use outlining is for reusing an object.
1006
+ false, // It makes no sense for that use case to be contextual.
1007
+ rootContextSnapshot, // Therefore we don't pass any contextual information along.
1008
request.abortableTasks,
1009
);
1010
retryTask(request, newTask);
1137
key: string,
1138
value: ReactClientValue,
1139
): ReactJSONValue {
1140
+ const prevKeyPath = task.keyPath;
1141
+ const prevImplicitSlot = task.implicitSlot;
1142
try {
1143
return renderModelDestructive(request, task, parent, key, value);
1144
} catch (thrownValue) {
1167
const newTask = createTask(
1168
request,
1169
task.model,
1019
- getActiveContext(),
1170
+ task.keyPath,
1171
+ task.implicitSlot,
1172
+ task.context,
1173
request.abortableTasks,
1174
);
1175
const ping = newTask.ping;
1176
(x: any).then(ping, ping);
1177
newTask.thenableState = getThenableStateAfterSuspending();
1178
+
1179
+ // Restore the context. We assume that this will be restored by the inner
1180
+ // functions in case nothing throws so we don't use "finally" here.
1181
+ task.keyPath = prevKeyPath;
1182
+ task.implicitSlot = prevImplicitSlot;
1183
+
1184
if (wasReactNode) {
1185
return serializeLazyID(newTask.id);
1186
}
1193
const postponeId = request.nextChunkId++;
1194
logPostpone(request, postponeInstance.message);
1195
emitPostponeChunk(request, postponeId, postponeInstance);
1196
+
1197
+ // Restore the context. We assume that this will be restored by the inner
1198
+ // functions in case nothing throws so we don't use "finally" here.
1199
+ task.keyPath = prevKeyPath;
1200
+ task.implicitSlot = prevImplicitSlot;
1201
+
1202
if (wasReactNode) {
1203
return serializeLazyID(postponeId);
1204
}
1205
return serializeByValueID(postponeId);
1206
}
1207
}
1208
+
1209
+ // Restore the context. We assume that this will be restored by the inner
1210
+ // functions in case nothing throws so we don't use "finally" here.
1211
+ task.keyPath = prevKeyPath;
1212
+ task.implicitSlot = prevImplicitSlot;
1213
+
1214
if (wasReactNode) {
1215
// Something errored. We'll still send everything we have up until this point.
1216
// We'll replace this element with a lazy reference that throws on the client
1260
const writtenObjects = request.writtenObjects;
1261
const existingId = writtenObjects.get(value);
1262
if (existingId !== undefined) {
1092
- if (existingId === -1) {
1093
- // Seen but not yet outlined.
1094
- const newId = outlineModel(request, value);
1095
- return serializeByValueID(newId);
1263
+ if (
1264
+ enableServerComponentKeys &&
1265
+ (task.keyPath !== null ||
1266
+ task.implicitSlot ||
1267
+ task.context !== rootContextSnapshot)
1268
+ ) {
1269
+ // If we're in some kind of context we can't reuse the result of this render or
1270
+ // previous renders of this element. We only reuse elements if they're not wrapped
1271
+ // by another Server Component.
1272
} else if (modelRoot === value) {
1273
// This is the ID we're currently emitting so we need to write it
1274
// once but if we discover it again, we refer to it by id.
1275
modelRoot = null;
1276
+ } else if (existingId === -1) {
1277
+ // Seen but not yet outlined.
1278
+ // TODO: If we throw here we can treat this as suspending which causes an outline
1279
+ // but that is able to reuse the same task if we're already in one but then that
1280
+ // will be a lazy future value rather than guaranteed to exist but maybe that's good.
1281
+ const newId = outlineModel(request, (value: any));
1282
+ return serializeLazyID(newId);
1283
} else {
1101
- // We've already emitted this as an outlined object, so we can
1102
- // just refer to that by its existing ID.
1103
- return serializeByValueID(existingId);
1284
+ // We've already emitted this as an outlined object, so we can refer to that by its
1285
+ // existing ID. We use a lazy reference since, unlike plain objects, elements might
1286
+ // suspend so it might not have emitted yet even if we have the ID for it.
1287
+ return serializeLazyID(existingId);
1288
}
1289
} else {
1290
// This is the first time we've seen this object. We may never see it again
1292
writtenObjects.set(value, -1);
1293
}
1294
1111
- // TODO: Concatenate keys of parents onto children.
1295
const element: React$Element<any> = (value: any);
1296
// Attempt to render the Server Component.
1297
return renderElement(
1298
request,
1299
task,
1300
element.type,
1301
+ // $FlowFixMe[incompatible-call] the key of an element is null | string
1302
element.key,
1303
element.ref,
1304
element.props,
1339
// $FlowFixMe[method-unbinding]
1340
if (typeof value.then === 'function') {
1341
if (existingId !== undefined) {
1158
- if (modelRoot === value) {
1342
+ if (
1343
+ enableServerComponentKeys &&
1344
+ (task.keyPath !== null ||
1345
+ task.implicitSlot ||
1346
+ task.context !== rootContextSnapshot)
1347
+ ) {
1348
+ // If we're in some kind of context we can't reuse the result of this render or
1349
+ // previous renders of this element. We only reuse Promises if they're not wrapped
1350
+ // by another Server Component.
1351
+ const promiseId = serializeThenable(request, task, (value: any));
1352
+ return serializePromiseID(promiseId);
1353
+ } else if (modelRoot === value) {
1354
// This is the ID we're currently emitting so we need to write it
1355
// once but if we discover it again, we refer to it by id.
1356
modelRoot = null;
1361
}
1362
// We assume that any object with a .then property is a "Thenable" type,
1363
// or a Promise type. Either of which can be represented by a Promise.
1169
- const promiseId = serializeThenable(request, (value: any));
1364
+ const promiseId = serializeThenable(request, task, (value: any));
1365
writtenObjects.set(value, promiseId);
1366
return serializePromiseID(promiseId);
1367
}
1390
}
1391
1392
if (existingId !== undefined) {
1198
- if (existingId === -1) {
1199
- // Seen but not yet outlined.
1200
- const newId = outlineModel(request, value);
1201
- return serializeByValueID(newId);
1202
- } else if (modelRoot === value) {
1393
+ if (modelRoot === value) {
1394
// This is the ID we're currently emitting so we need to write it
1395
// once but if we discover it again, we refer to it by id.
1396
modelRoot = null;
1397
+ } else if (existingId === -1) {
1398
+ // Seen but not yet outlined.
1399
+ const newId = outlineModel(request, (value: any));
1400
+ return serializeByValueID(newId);
1401
} else {
1402
// We've already emitted this as an outlined object, so we can
1403
// just refer to that by its existing ID.
1410
}
1411
1412
if (isArray(value)) {
1218
- // $FlowFixMe[incompatible-return]
1219
- return value;
1413
+ return renderFragment(request, task, value);
1414
}
1415
1416
if (value instanceof Map) {
1476
1477
const iteratorFn = getIteratorFn(value);
1478
if (iteratorFn) {
1285
- return Array.from((value: any));
1479
+ return renderFragment(request, task, Array.from((value: any)));
1480
}
1481
1482
// Verify that this is a simple plain object.
1776
return;
1777
}
1778
1779
+ const prevContext = getActiveContext();
1780
switchContext(task.context);
1781
try {
1782
// Track the root so we know that we have to emit this object even though it
1797
// Track the root again for the resolved object.
1798
modelRoot = resolvedModel;
1799
1605
- // If the value is a string, it means it's a terminal value adn we already escaped it
1606
- // We don't need to escape it again so it's not passed the toJSON replacer.
1607
- // Object might contain unresolved values like additional elements.
1608
- // This is simulating what the JSON loop would do if this was part of it.
1609
- // $FlowFixMe[incompatible-type] stringify can return null
1610
- const json: string =
1611
- typeof resolvedModel === 'string'
1612
- ? stringify(resolvedModel)
1613
- : stringify(resolvedModel, task.toJSON);
1800
+ // The keyPath resets at any terminal child node.
1801
+ task.keyPath = null;
1802
+ task.implicitSlot = false;
1803
+
1804
+ let json: string;
1805
+ if (typeof resolvedModel === 'object' && resolvedModel !== null) {
1806
+ // Object might contain unresolved values like additional elements.
1807
+ // This is simulating what the JSON loop would do if this was part of it.
1808
+ // $FlowFixMe[incompatible-type] stringify can return null for undefined but we never do
1809
+ json = stringify(resolvedModel, task.toJSON);
1810
+ } else {
1811
+ // If the value is a string, it means it's a terminal value and we already escaped it
1812
+ // We don't need to escape it again so it's not passed the toJSON replacer.
1813
+ // $FlowFixMe[incompatible-type] stringify can return null for undefined but we never do
1814
+ json = stringify(resolvedModel);
1815
+ }
1816
emitModelChunk(request, task.id, json);
1817
1818
request.abortableTasks.delete(task);
1848
task.status = ERRORED;
1849
const digest = logRecoverableError(request, x);
1850
emitErrorChunk(request, task.id, digest, x);
1851
+ } finally {
1852
+ if (enableServerContext) {
1853
+ switchContext(prevContext);
1854
+ }
1855
}
1856
}
1857