[Fizz] Track Key Path (#27243)
Tracks the currently executing parent path of a task, using the name of the component and the key or index. This can be used to uniquely identify an instance of a component between requests - assuming nothing in the parents has changed. Even if it has changed, if things are properly keyed, it should still line up. It's not used yet but we'll need this for two separate features so should land this so we can stack on top. Can be passed to `JSON.stringify(...)` to generate a unique key.
Sebastian Markbåge committed
Aug 18, 2023 at 10:18 UTC
98f3f14d2e06fb785103296318204cd154d5d0ed
1 file changed
+75
-24
packages/react-server/src/ReactFizzServer.js
+75
-24
@@ -151,6 +151,13 @@ const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
151
const ReactCurrentCache = ReactSharedInternals.ReactCurrentCache;
152
const ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;
153
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
+
161
type LegacyContext = {
162
[key: string]: any,
163
};
@@ -176,6 +183,7 @@ export type Task = {
183
blockedBoundary: Root | SuspenseBoundary,
184
blockedSegment: Segment, // the segment we'll write to
185
abortSet: Set<Task>, // the abortable set that this task belongs to
186
+ keyPath: KeyNode, // the path of all parent keys currently rendering
187
legacyContext: LegacyContext, // the current legacy context that this task is executing in
188
context: ContextSnapshot, // the current new context that this task is executing in
189
treeContext: TreeContext, // the current tree context that this task is executing in
@@ -335,6 +343,7 @@ export function createRequest(
343
null,
344
rootSegment,
345
abortSet,
346
+ null,
347
emptyContextObject,
348
rootContextSnapshot,
349
emptyTreeContext,
@@ -388,6 +397,7 @@ function createTask(
397
blockedBoundary: Root | SuspenseBoundary,
398
blockedSegment: Segment,
399
abortSet: Set<Task>,
400
+ keyPath: KeyNode,
401
legacyContext: LegacyContext,
402
context: ContextSnapshot,
403
treeContext: TreeContext,
@@ -404,6 +414,7 @@ function createTask(
414
blockedBoundary,
415
blockedSegment,
416
abortSet,
417
+ keyPath,
418
legacyContext,
419
context,
420
treeContext,
@@ -617,7 +628,7 @@ function renderSuspenseBoundary(
628
}
629
try {
630
// 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);
632
pushSegmentFinale(
633
contentRootSegment.chunks,
634
request.responseState,
@@ -678,6 +689,7 @@ function renderSuspenseBoundary(
689
parentBoundary,
690
boundarySegment,
691
fallbackAbortSet,
692
+ task.keyPath,
693
task.legacyContext,
694
task.context,
695
task.treeContext,
@@ -703,7 +715,7 @@ function renderBackupSuspenseBoundary(
715
const segment = task.blockedSegment;
716
717
pushStartCompletedSuspenseBoundary(segment.chunks);
706
- renderNode(request, task, content);
718
+ renderNode(request, task, content, 0);
719
pushEndCompletedSuspenseBoundary(segment.chunks);
720
721
popComponentStackInDEV(task);
@@ -733,7 +745,7 @@ function renderHostElement(
745
746
// We use the non-destructive form because if something suspends, we still
747
// need to pop back up and finish this subtree of HTML.
736
- renderNode(request, task, children);
748
+ renderNode(request, task, children, 0);
749
750
// We expect that errors will fatal the whole task and that we don't need
751
// the correct context. Therefore this is not in a finally.
@@ -800,13 +812,13 @@ function finishClassComponent(
812
childContextTypes,
813
);
814
task.legacyContext = mergedContext;
803
- renderNodeDestructive(request, task, null, nextChildren);
815
+ renderNodeDestructive(request, task, null, nextChildren, 0);
816
task.legacyContext = previousContext;
817
return;
818
}
819
}
820
809
- renderNodeDestructive(request, task, null, nextChildren);
821
+ renderNodeDestructive(request, task, null, nextChildren, 0);
822
}
823
824
function renderClassComponent(
@@ -957,12 +969,12 @@ function renderIndeterminateComponent(
969
const index = 0;
970
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index);
971
try {
960
- renderNodeDestructive(request, task, null, value);
972
+ renderNodeDestructive(request, task, null, value, 0);
973
} finally {
974
task.treeContext = prevTreeContext;
975
}
976
} else {
965
- renderNodeDestructive(request, task, null, value);
977
+ renderNodeDestructive(request, task, null, value, 0);
978
}
979
}
980
popComponentStackInDEV(task);
@@ -1062,12 +1074,12 @@ function renderForwardRef(
1074
const index = 0;
1075
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index);
1076
try {
1065
- renderNodeDestructive(request, task, null, children);
1077
+ renderNodeDestructive(request, task, null, children, 0);
1078
} finally {
1079
task.treeContext = prevTreeContext;
1080
}
1081
} else {
1070
- renderNodeDestructive(request, task, null, children);
1082
+ renderNodeDestructive(request, task, null, children, 0);
1083
}
1084
popComponentStackInDEV(task);
1085
}
@@ -1139,7 +1151,7 @@ function renderContextConsumer(
1151
const newValue = readContext(context);
1152
const newChildren = render(newValue);
1153
1142
- renderNodeDestructive(request, task, null, newChildren);
1154
+ renderNodeDestructive(request, task, null, newChildren, 0);
1155
}
1156
1157
function renderContextProvider(
@@ -1156,7 +1168,7 @@ function renderContextProvider(
1168
prevSnapshot = task.context;
1169
}
1170
task.context = pushProvider(context, value);
1159
- renderNodeDestructive(request, task, null, children);
1171
+ renderNodeDestructive(request, task, null, children, 0);
1172
task.context = popProvider(context);
1173
if (__DEV__) {
1174
if (prevSnapshot !== task.context) {
@@ -1199,7 +1211,7 @@ function renderOffscreen(request: Request, task: Task, props: Object): void {
1211
} else {
1212
// A visible Offscreen boundary is treated exactly like a fragment: a
1213
// pure indirection.
1202
- renderNodeDestructive(request, task, null, props.children);
1214
+ renderNodeDestructive(request, task, null, props.children, 0);
1215
}
1216
}
1217
@@ -1246,7 +1258,7 @@ function renderElement(
1258
case REACT_STRICT_MODE_TYPE:
1259
case REACT_PROFILER_TYPE:
1260
case REACT_FRAGMENT_TYPE: {
1249
- renderNodeDestructive(request, task, null, props.children);
1261
+ renderNodeDestructive(request, task, null, props.children, 0);
1262
return;
1263
}
1264
case REACT_OFFSCREEN_TYPE: {
@@ -1256,13 +1268,13 @@ function renderElement(
1268
case REACT_SUSPENSE_LIST_TYPE: {
1269
pushBuiltInComponentStackInDEV(task, 'SuspenseList');
1270
// TODO: SuspenseList should control the boundaries.
1259
- renderNodeDestructive(request, task, null, props.children);
1271
+ renderNodeDestructive(request, task, null, props.children, 0);
1272
popComponentStackInDEV(task);
1273
return;
1274
}
1275
case REACT_SCOPE_TYPE: {
1276
if (enableScopeAPI) {
1265
- renderNodeDestructive(request, task, null, props.children);
1277
+ renderNodeDestructive(request, task, null, props.children, 0);
1278
return;
1279
}
1280
throw new Error('ReactDOMServer does not yet support scope components.');
@@ -1368,13 +1380,20 @@ function renderNodeDestructive(
1380
// always null, except when called by retryTask.
1381
prevThenableState: ThenableState | null,
1382
node: ReactNodeList,
1383
+ childIndex: number,
1384
): void {
1385
if (__DEV__) {
1386
// In Dev we wrap renderNodeDestructiveImpl in a try / catch so we can capture
1387
// a component stack at the right place in the tree. We don't do this in renderNode
1388
// becuase it is not called at every layer of the tree and we may lose frames
1389
try {
1377
- return renderNodeDestructiveImpl(request, task, prevThenableState, node);
1390
+ return renderNodeDestructiveImpl(
1391
+ request,
1392
+ task,
1393
+ prevThenableState,
1394
+ node,
1395
+ childIndex,
1396
+ );
1397
} catch (x) {
1398
if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
1399
// This is a Wakable, noop
@@ -1389,7 +1408,13 @@ function renderNodeDestructive(
1408
throw x;
1409
}
1410
} else {
1392
- return renderNodeDestructiveImpl(request, task, prevThenableState, node);
1411
+ return renderNodeDestructiveImpl(
1412
+ request,
1413
+ task,
1414
+ prevThenableState,
1415
+ node,
1416
+ childIndex,
1417
+ );
1418
}
1419
}
1420
@@ -1400,6 +1425,7 @@ function renderNodeDestructiveImpl(
1425
task: Task,
1426
prevThenableState: ThenableState | null,
1427
node: ReactNodeList,
1428
+ childIndex: number,
1429
): void {
1430
// Stash the node we're working on. We'll pick up from this task in case
1431
// something suspends.
@@ -1411,9 +1437,14 @@ function renderNodeDestructiveImpl(
1437
case REACT_ELEMENT_TYPE: {
1438
const element: React$Element<any> = (node: any);
1439
const type = element.type;
1440
+ const key = element.key;
1441
const props = element.props;
1442
const ref = element.ref;
1443
+ const name = getComponentNameFromType(type);
1444
+ const prevKeyPath = task.keyPath;
1445
+ task.keyPath = [task.keyPath, name, key == null ? childIndex : key];
1446
renderElement(request, task, prevThenableState, type, props, ref);
1447
+ task.keyPath = prevKeyPath;
1448
return;
1449
}
1450
case REACT_PORTAL_TYPE:
@@ -1446,13 +1477,13 @@ function renderNodeDestructiveImpl(
1477
} else {
1478
resolvedNode = init(payload);
1479
}
1449
- renderNodeDestructive(request, task, null, resolvedNode);
1480
+ renderNodeDestructive(request, task, null, resolvedNode, childIndex);
1481
return;
1482
}
1483
}
1484
1485
if (isArray(node)) {
1455
- renderChildrenArray(request, task, node);
1486
+ renderChildrenArray(request, task, node, childIndex);
1487
return;
1488
}
1489
@@ -1476,7 +1507,7 @@ function renderNodeDestructiveImpl(
1507
children.push(step.value);
1508
step = iterator.next();
1509
} while (!step.done);
1479
- renderChildrenArray(request, task, children);
1510
+ renderChildrenArray(request, task, children, childIndex);
1511
return;
1512
}
1513
return;
@@ -1500,6 +1531,7 @@ function renderNodeDestructiveImpl(
1531
task,
1532
null,
1533
unwrapThenable(thenable),
1534
+ childIndex,
1535
);
1536
}
1537
@@ -1513,6 +1545,7 @@ function renderNodeDestructiveImpl(
1545
task,
1546
null,
1547
readContext(context),
1548
+ childIndex,
1549
);
1550
}
1551
@@ -1567,17 +1600,26 @@ function renderChildrenArray(
1600
request: Request,
1601
task: Task,
1602
children: Array<any>,
1603
+ childIndex: number,
1604
) {
1605
+ const prevKeyPath = task.keyPath;
1606
const totalChildren = children.length;
1607
for (let i = 0; i < totalChildren; i++) {
1608
const prevTreeContext = task.treeContext;
1609
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i);
1610
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
+ }
1617
// We need to use the non-destructive form so that we can safely pop back
1618
// up and render the sibling if something suspends.
1578
- renderNode(request, task, children[i]);
1619
+ renderNode(request, task, node, i);
1620
} finally {
1621
task.treeContext = prevTreeContext;
1622
+ task.keyPath = prevKeyPath;
1623
}
1624
}
1625
}
@@ -1611,6 +1653,7 @@ function spawnNewSuspendedTask(
1653
task.blockedBoundary,
1654
newSegment,
1655
task.abortSet,
1656
+ task.keyPath,
1657
task.legacyContext,
1658
task.context,
1659
task.treeContext,
@@ -1629,7 +1672,12 @@ function spawnNewSuspendedTask(
1672
1673
// This is a non-destructive form of rendering a node. If it suspends it spawns
1674
// 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 {
1681
// Store how much we've pushed at this point so we can reset it in case something
1682
// suspended partially through writing something.
1683
const segment = task.blockedSegment;
@@ -1641,12 +1689,13 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
1689
const previousFormatContext = task.blockedSegment.formatContext;
1690
const previousLegacyContext = task.legacyContext;
1691
const previousContext = task.context;
1692
+ const previousKeyPath = task.keyPath;
1693
let previousComponentStack = null;
1694
if (__DEV__) {
1695
previousComponentStack = task.componentStack;
1696
}
1697
try {
1649
- return renderNodeDestructive(request, task, null, node);
1698
+ return renderNodeDestructive(request, task, null, node, childIndex);
1699
} catch (thrownValue) {
1700
resetHooksState();
1701
@@ -1675,6 +1724,7 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
1724
task.blockedSegment.formatContext = previousFormatContext;
1725
task.legacyContext = previousLegacyContext;
1726
task.context = previousContext;
1727
+ task.keyPath = previousKeyPath;
1728
// Restore all active ReactContexts to what they were before.
1729
switchContext(previousContext);
1730
if (__DEV__) {
@@ -1687,6 +1737,7 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
1737
task.blockedSegment.formatContext = previousFormatContext;
1738
task.legacyContext = previousLegacyContext;
1739
task.context = previousContext;
1740
+ task.keyPath = previousKeyPath;
1741
// Restore all active ReactContexts to what they were before.
1742
switchContext(previousContext);
1743
if (__DEV__) {
@@ -1955,7 +2006,7 @@ function retryTask(request: Request, task: Task): void {
2006
const prevThenableState = task.thenableState;
2007
task.thenableState = null;
2008
1958
- renderNodeDestructive(request, task, prevThenableState, task.node);
2009
+ renderNodeDestructive(request, task, prevThenableState, task.node, 0);
2010
pushSegmentFinale(
2011
segment.chunks,
2012
request.responseState,