Skip to content

Commit 6aea9f9

Browse files
committed
Track Key Path
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 and instance of a component between requests - assuming nothing has changed.
1 parent 5623f2a commit 6aea9f9

File tree

1 file changed

+75
-24
lines changed

1 file changed

+75
-24
lines changed

packages/react-server/src/ReactFizzServer.js

Lines changed: 75 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,13 @@ const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
151151
const ReactCurrentCache = ReactSharedInternals.ReactCurrentCache;
152152
const ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;
153153

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+
154161
type LegacyContext = {
155162
[key: string]: any,
156163
};
@@ -176,6 +183,7 @@ export type Task = {
176183
blockedBoundary: Root | SuspenseBoundary,
177184
blockedSegment: Segment, // the segment we'll write to
178185
abortSet: Set<Task>, // the abortable set that this task belongs to
186+
keyPath: KeyNode, // the path of all parent keys currently rendering
179187
legacyContext: LegacyContext, // the current legacy context that this task is executing in
180188
context: ContextSnapshot, // the current new context that this task is executing in
181189
treeContext: TreeContext, // the current tree context that this task is executing in
@@ -335,6 +343,7 @@ export function createRequest(
335343
null,
336344
rootSegment,
337345
abortSet,
346+
null,
338347
emptyContextObject,
339348
rootContextSnapshot,
340349
emptyTreeContext,
@@ -388,6 +397,7 @@ function createTask(
388397
blockedBoundary: Root | SuspenseBoundary,
389398
blockedSegment: Segment,
390399
abortSet: Set<Task>,
400+
keyPath: KeyNode,
391401
legacyContext: LegacyContext,
392402
context: ContextSnapshot,
393403
treeContext: TreeContext,
@@ -404,6 +414,7 @@ function createTask(
404414
blockedBoundary,
405415
blockedSegment,
406416
abortSet,
417+
keyPath,
407418
legacyContext,
408419
context,
409420
treeContext,
@@ -617,7 +628,7 @@ function renderSuspenseBoundary(
617628
}
618629
try {
619630
// 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);
621632
pushSegmentFinale(
622633
contentRootSegment.chunks,
623634
request.responseState,
@@ -678,6 +689,7 @@ function renderSuspenseBoundary(
678689
parentBoundary,
679690
boundarySegment,
680691
fallbackAbortSet,
692+
task.keyPath,
681693
task.legacyContext,
682694
task.context,
683695
task.treeContext,
@@ -703,7 +715,7 @@ function renderBackupSuspenseBoundary(
703715
const segment = task.blockedSegment;
704716

705717
pushStartCompletedSuspenseBoundary(segment.chunks);
706-
renderNode(request, task, content);
718+
renderNode(request, task, content, 0);
707719
pushEndCompletedSuspenseBoundary(segment.chunks);
708720

709721
popComponentStackInDEV(task);
@@ -733,7 +745,7 @@ function renderHostElement(
733745

734746
// We use the non-destructive form because if something suspends, we still
735747
// need to pop back up and finish this subtree of HTML.
736-
renderNode(request, task, children);
748+
renderNode(request, task, children, 0);
737749

738750
// We expect that errors will fatal the whole task and that we don't need
739751
// the correct context. Therefore this is not in a finally.
@@ -800,13 +812,13 @@ function finishClassComponent(
800812
childContextTypes,
801813
);
802814
task.legacyContext = mergedContext;
803-
renderNodeDestructive(request, task, null, nextChildren);
815+
renderNodeDestructive(request, task, null, nextChildren, 0);
804816
task.legacyContext = previousContext;
805817
return;
806818
}
807819
}
808820

809-
renderNodeDestructive(request, task, null, nextChildren);
821+
renderNodeDestructive(request, task, null, nextChildren, 0);
810822
}
811823

812824
function renderClassComponent(
@@ -957,12 +969,12 @@ function renderIndeterminateComponent(
957969
const index = 0;
958970
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index);
959971
try {
960-
renderNodeDestructive(request, task, null, value);
972+
renderNodeDestructive(request, task, null, value, 0);
961973
} finally {
962974
task.treeContext = prevTreeContext;
963975
}
964976
} else {
965-
renderNodeDestructive(request, task, null, value);
977+
renderNodeDestructive(request, task, null, value, 0);
966978
}
967979
}
968980
popComponentStackInDEV(task);
@@ -1062,12 +1074,12 @@ function renderForwardRef(
10621074
const index = 0;
10631075
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index);
10641076
try {
1065-
renderNodeDestructive(request, task, null, children);
1077+
renderNodeDestructive(request, task, null, children, 0);
10661078
} finally {
10671079
task.treeContext = prevTreeContext;
10681080
}
10691081
} else {
1070-
renderNodeDestructive(request, task, null, children);
1082+
renderNodeDestructive(request, task, null, children, 0);
10711083
}
10721084
popComponentStackInDEV(task);
10731085
}
@@ -1139,7 +1151,7 @@ function renderContextConsumer(
11391151
const newValue = readContext(context);
11401152
const newChildren = render(newValue);
11411153

1142-
renderNodeDestructive(request, task, null, newChildren);
1154+
renderNodeDestructive(request, task, null, newChildren, 0);
11431155
}
11441156

11451157
function renderContextProvider(
@@ -1156,7 +1168,7 @@ function renderContextProvider(
11561168
prevSnapshot = task.context;
11571169
}
11581170
task.context = pushProvider(context, value);
1159-
renderNodeDestructive(request, task, null, children);
1171+
renderNodeDestructive(request, task, null, children, 0);
11601172
task.context = popProvider(context);
11611173
if (__DEV__) {
11621174
if (prevSnapshot !== task.context) {
@@ -1199,7 +1211,7 @@ function renderOffscreen(request: Request, task: Task, props: Object): void {
11991211
} else {
12001212
// A visible Offscreen boundary is treated exactly like a fragment: a
12011213
// pure indirection.
1202-
renderNodeDestructive(request, task, null, props.children);
1214+
renderNodeDestructive(request, task, null, props.children, 0);
12031215
}
12041216
}
12051217

@@ -1246,7 +1258,7 @@ function renderElement(
12461258
case REACT_STRICT_MODE_TYPE:
12471259
case REACT_PROFILER_TYPE:
12481260
case REACT_FRAGMENT_TYPE: {
1249-
renderNodeDestructive(request, task, null, props.children);
1261+
renderNodeDestructive(request, task, null, props.children, 0);
12501262
return;
12511263
}
12521264
case REACT_OFFSCREEN_TYPE: {
@@ -1256,13 +1268,13 @@ function renderElement(
12561268
case REACT_SUSPENSE_LIST_TYPE: {
12571269
pushBuiltInComponentStackInDEV(task, 'SuspenseList');
12581270
// TODO: SuspenseList should control the boundaries.
1259-
renderNodeDestructive(request, task, null, props.children);
1271+
renderNodeDestructive(request, task, null, props.children, 0);
12601272
popComponentStackInDEV(task);
12611273
return;
12621274
}
12631275
case REACT_SCOPE_TYPE: {
12641276
if (enableScopeAPI) {
1265-
renderNodeDestructive(request, task, null, props.children);
1277+
renderNodeDestructive(request, task, null, props.children, 0);
12661278
return;
12671279
}
12681280
throw new Error('ReactDOMServer does not yet support scope components.');
@@ -1368,13 +1380,20 @@ function renderNodeDestructive(
13681380
// always null, except when called by retryTask.
13691381
prevThenableState: ThenableState | null,
13701382
node: ReactNodeList,
1383+
childIndex: number,
13711384
): void {
13721385
if (__DEV__) {
13731386
// In Dev we wrap renderNodeDestructiveImpl in a try / catch so we can capture
13741387
// a component stack at the right place in the tree. We don't do this in renderNode
13751388
// becuase it is not called at every layer of the tree and we may lose frames
13761389
try {
1377-
return renderNodeDestructiveImpl(request, task, prevThenableState, node);
1390+
return renderNodeDestructiveImpl(
1391+
request,
1392+
task,
1393+
prevThenableState,
1394+
node,
1395+
childIndex,
1396+
);
13781397
} catch (x) {
13791398
if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
13801399
// This is a Wakable, noop
@@ -1389,7 +1408,13 @@ function renderNodeDestructive(
13891408
throw x;
13901409
}
13911410
} else {
1392-
return renderNodeDestructiveImpl(request, task, prevThenableState, node);
1411+
return renderNodeDestructiveImpl(
1412+
request,
1413+
task,
1414+
prevThenableState,
1415+
node,
1416+
childIndex,
1417+
);
13931418
}
13941419
}
13951420

@@ -1400,6 +1425,7 @@ function renderNodeDestructiveImpl(
14001425
task: Task,
14011426
prevThenableState: ThenableState | null,
14021427
node: ReactNodeList,
1428+
childIndex: number,
14031429
): void {
14041430
// Stash the node we're working on. We'll pick up from this task in case
14051431
// something suspends.
@@ -1411,9 +1437,14 @@ function renderNodeDestructiveImpl(
14111437
case REACT_ELEMENT_TYPE: {
14121438
const element: React$Element<any> = (node: any);
14131439
const type = element.type;
1440+
const key = element.key;
14141441
const props = element.props;
14151442
const ref = element.ref;
1443+
const name = getComponentNameFromType(type);
1444+
const prevKeyPath = task.keyPath;
1445+
task.keyPath = [task.keyPath, name, key == null ? childIndex : key];
14161446
renderElement(request, task, prevThenableState, type, props, ref);
1447+
task.keyPath = prevKeyPath;
14171448
return;
14181449
}
14191450
case REACT_PORTAL_TYPE:
@@ -1446,13 +1477,13 @@ function renderNodeDestructiveImpl(
14461477
} else {
14471478
resolvedNode = init(payload);
14481479
}
1449-
renderNodeDestructive(request, task, null, resolvedNode);
1480+
renderNodeDestructive(request, task, null, resolvedNode, childIndex);
14501481
return;
14511482
}
14521483
}
14531484

14541485
if (isArray(node)) {
1455-
renderChildrenArray(request, task, node);
1486+
renderChildrenArray(request, task, node, childIndex);
14561487
return;
14571488
}
14581489

@@ -1476,7 +1507,7 @@ function renderNodeDestructiveImpl(
14761507
children.push(step.value);
14771508
step = iterator.next();
14781509
} while (!step.done);
1479-
renderChildrenArray(request, task, children);
1510+
renderChildrenArray(request, task, children, childIndex);
14801511
return;
14811512
}
14821513
return;
@@ -1500,6 +1531,7 @@ function renderNodeDestructiveImpl(
15001531
task,
15011532
null,
15021533
unwrapThenable(thenable),
1534+
childIndex,
15031535
);
15041536
}
15051537

@@ -1513,6 +1545,7 @@ function renderNodeDestructiveImpl(
15131545
task,
15141546
null,
15151547
readContext(context),
1548+
childIndex,
15161549
);
15171550
}
15181551

@@ -1567,17 +1600,26 @@ function renderChildrenArray(
15671600
request: Request,
15681601
task: Task,
15691602
children: Array<any>,
1603+
childIndex: number,
15701604
) {
1605+
const prevKeyPath = task.keyPath;
15711606
const totalChildren = children.length;
15721607
for (let i = 0; i < totalChildren; i++) {
15731608
const prevTreeContext = task.treeContext;
15741609
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i);
15751610
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+
}
15761617
// We need to use the non-destructive form so that we can safely pop back
15771618
// up and render the sibling if something suspends.
1578-
renderNode(request, task, children[i]);
1619+
renderNode(request, task, node, i);
15791620
} finally {
15801621
task.treeContext = prevTreeContext;
1622+
task.keyPath = prevKeyPath;
15811623
}
15821624
}
15831625
}
@@ -1611,6 +1653,7 @@ function spawnNewSuspendedTask(
16111653
task.blockedBoundary,
16121654
newSegment,
16131655
task.abortSet,
1656+
task.keyPath,
16141657
task.legacyContext,
16151658
task.context,
16161659
task.treeContext,
@@ -1629,7 +1672,12 @@ function spawnNewSuspendedTask(
16291672

16301673
// This is a non-destructive form of rendering a node. If it suspends it spawns
16311674
// 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 {
16331681
// Store how much we've pushed at this point so we can reset it in case something
16341682
// suspended partially through writing something.
16351683
const segment = task.blockedSegment;
@@ -1641,12 +1689,13 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
16411689
const previousFormatContext = task.blockedSegment.formatContext;
16421690
const previousLegacyContext = task.legacyContext;
16431691
const previousContext = task.context;
1692+
const previousKeyPath = task.keyPath;
16441693
let previousComponentStack = null;
16451694
if (__DEV__) {
16461695
previousComponentStack = task.componentStack;
16471696
}
16481697
try {
1649-
return renderNodeDestructive(request, task, null, node);
1698+
return renderNodeDestructive(request, task, null, node, childIndex);
16501699
} catch (thrownValue) {
16511700
resetHooksState();
16521701

@@ -1675,6 +1724,7 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
16751724
task.blockedSegment.formatContext = previousFormatContext;
16761725
task.legacyContext = previousLegacyContext;
16771726
task.context = previousContext;
1727+
task.keyPath = previousKeyPath;
16781728
// Restore all active ReactContexts to what they were before.
16791729
switchContext(previousContext);
16801730
if (__DEV__) {
@@ -1687,6 +1737,7 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
16871737
task.blockedSegment.formatContext = previousFormatContext;
16881738
task.legacyContext = previousLegacyContext;
16891739
task.context = previousContext;
1740+
task.keyPath = previousKeyPath;
16901741
// Restore all active ReactContexts to what they were before.
16911742
switchContext(previousContext);
16921743
if (__DEV__) {
@@ -1955,7 +2006,7 @@ function retryTask(request: Request, task: Task): void {
19552006
const prevThenableState = task.thenableState;
19562007
task.thenableState = null;
19572008

1958-
renderNodeDestructive(request, task, prevThenableState, task.node);
2009+
renderNodeDestructive(request, task, prevThenableState, task.node, 0);
19592010
pushSegmentFinale(
19602011
segment.chunks,
19612012
request.responseState,

0 commit comments

Comments
 (0)