@samitouri / QOS-React-1 / commits / 98f3f14d2e

[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,