Skip to content

Commit 98f3f14

Browse files
authored
[Fizz] Track Key Path (facebook#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.
1 parent 5623f2a commit 98f3f14

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)