Skip to content

Commit 7e018e7

Browse files
committed
Track the key path difference between right before the first array and after
There's a subtle difference if you suspend before the first array or after. In Fiber, we don't deal with this because we just suspend the parent and replay it if lazy() or Usable are used in its child slots. In Fizz we try to optimize this a bit more and enable resuming inside the component. Semantically, it's different if you suspend/postpone before the first child array or inside that child array. Because when you resume the inner result might be another array and either that's part of the parent path or part of the inner slot. There might be more clever way of structuring this but I just use -1 to indicate that we're not yet inside the array and is in the root child position. If that renders an element, then that's just the same as the 0 slot. We need to also encode this in the resuming.
1 parent bb1d8d1 commit 7e018e7

File tree

2 files changed

+50
-71
lines changed

2 files changed

+50
-71
lines changed

packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
461461
if (prerendering) {
462462
React.unstable_postpone();
463463
}
464-
return 'Hello';
464+
return ['Hello', 'World'];
465465
}
466466

467467
function App() {

packages/react-server/src/ReactFizzServer.js

Lines changed: 49 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,8 @@ type KeyNode = [
166166

167167
const REPLAY_NODE = 0;
168168
const REPLAY_SUSPENSE_BOUNDARY = 1;
169-
const RESUME_SEGMENT = 2;
169+
const RESUME_NODE = 2;
170+
const RESUME_ELEMENT = 3;
170171

171172
type ResumableParentNode =
172173
| [
@@ -185,9 +186,15 @@ type ResumableParentNode =
185186
type ResumableNode =
186187
| ResumableParentNode
187188
| [
188-
2, // RESUME_SEGMENT
189+
2, // RESUME_NODE
189190
number /* index */,
190191
number /* segment id */,
192+
]
193+
| [
194+
3, // RESUME_ELEMENT
195+
string | null /* name */,
196+
string | number /* key */,
197+
number /* segment id */,
191198
];
192199

193200
type PostponedHoles = {
@@ -784,7 +791,7 @@ function renderSuspenseBoundary(
784791
}
785792
try {
786793
// We use the safe form because we don't handle suspending here. Only error handling.
787-
renderNode(request, task, content, 0);
794+
renderNode(request, task, content, -1);
788795
pushSegmentFinale(
789796
contentRootSegment.chunks,
790797
request.renderState,
@@ -873,7 +880,7 @@ function renderBackupSuspenseBoundary(
873880
const segment = task.blockedSegment;
874881

875882
pushStartCompletedSuspenseBoundary(segment.chunks);
876-
renderNode(request, task, content, 0);
883+
renderNode(request, task, content, -1);
877884
pushEndCompletedSuspenseBoundary(segment.chunks);
878885

879886
popComponentStackInDEV(task);
@@ -903,7 +910,7 @@ function renderHostElement(
903910

904911
// We use the non-destructive form because if something suspends, we still
905912
// need to pop back up and finish this subtree of HTML.
906-
renderNode(request, task, children, 0);
913+
renderNode(request, task, children, -1);
907914

908915
// We expect that errors will fatal the whole task and that we don't need
909916
// the correct context. Therefore this is not in a finally.
@@ -970,13 +977,13 @@ function finishClassComponent(
970977
childContextTypes,
971978
);
972979
task.legacyContext = mergedContext;
973-
renderNodeDestructive(request, task, null, nextChildren, 0);
980+
renderNodeDestructive(request, task, null, nextChildren, -1);
974981
task.legacyContext = previousContext;
975982
return;
976983
}
977984
}
978985

979-
renderNodeDestructive(request, task, null, nextChildren, 0);
986+
renderNodeDestructive(request, task, null, nextChildren, -1);
980987
}
981988

982989
function renderClassComponent(
@@ -1170,20 +1177,20 @@ function finishFunctionComponent(
11701177
// Modify the id context. Because we'll need to reset this if something
11711178
// suspends or errors, we'll use the non-destructive render path.
11721179
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index);
1173-
renderNode(request, task, children, 0);
1180+
renderNode(request, task, children, -1);
11741181
// Like the other contexts, this does not need to be in a finally block
11751182
// because renderNode takes care of unwinding the stack.
11761183
task.treeContext = prevTreeContext;
11771184
} else if (didEmitFormStateMarkers) {
11781185
// If there were formState hooks, we must use the non-destructive path
11791186
// because this component is not a pure indirection; we emitted markers
11801187
// to the stream.
1181-
renderNode(request, task, children, 0);
1188+
renderNode(request, task, children, -1);
11821189
} else {
11831190
// We're now successfully past this task, and we haven't modified the
11841191
// context stack. We don't have to pop back to the previous task every
11851192
// again, so we can use the destructive recursive form.
1186-
renderNodeDestructive(request, task, null, children, 0);
1193+
renderNodeDestructive(request, task, null, children, -1);
11871194
}
11881195
}
11891196

@@ -1353,7 +1360,7 @@ function renderContextConsumer(
13531360
const newValue = readContext(context);
13541361
const newChildren = render(newValue);
13551362

1356-
renderNodeDestructive(request, task, null, newChildren, 0);
1363+
renderNodeDestructive(request, task, null, newChildren, -1);
13571364
}
13581365

13591366
function renderContextProvider(
@@ -1370,7 +1377,7 @@ function renderContextProvider(
13701377
prevSnapshot = task.context;
13711378
}
13721379
task.context = pushProvider(context, value);
1373-
renderNodeDestructive(request, task, null, children, 0);
1380+
renderNodeDestructive(request, task, null, children, -1);
13741381
task.context = popProvider(context);
13751382
if (__DEV__) {
13761383
if (prevSnapshot !== task.context) {
@@ -1413,7 +1420,7 @@ function renderOffscreen(request: Request, task: Task, props: Object): void {
14131420
} else {
14141421
// A visible Offscreen boundary is treated exactly like a fragment: a
14151422
// pure indirection.
1416-
renderNodeDestructive(request, task, null, props.children, 0);
1423+
renderNodeDestructive(request, task, null, props.children, -1);
14171424
}
14181425
}
14191426

@@ -1460,7 +1467,7 @@ function renderElement(
14601467
case REACT_STRICT_MODE_TYPE:
14611468
case REACT_PROFILER_TYPE:
14621469
case REACT_FRAGMENT_TYPE: {
1463-
renderNodeDestructive(request, task, null, props.children, 0);
1470+
renderNodeDestructive(request, task, null, props.children, -1);
14641471
return;
14651472
}
14661473
case REACT_OFFSCREEN_TYPE: {
@@ -1470,13 +1477,13 @@ function renderElement(
14701477
case REACT_SUSPENSE_LIST_TYPE: {
14711478
pushBuiltInComponentStackInDEV(task, 'SuspenseList');
14721479
// TODO: SuspenseList should control the boundaries.
1473-
renderNodeDestructive(request, task, null, props.children, 0);
1480+
renderNodeDestructive(request, task, null, props.children, -1);
14741481
popComponentStackInDEV(task);
14751482
return;
14761483
}
14771484
case REACT_SCOPE_TYPE: {
14781485
if (enableScopeAPI) {
1479-
renderNodeDestructive(request, task, null, props.children, 0);
1486+
renderNodeDestructive(request, task, null, props.children, -1);
14801487
return;
14811488
}
14821489
throw new Error('ReactDOMServer does not yet support scope components.');
@@ -1645,7 +1652,11 @@ function renderNodeDestructiveImpl(
16451652
const ref = element.ref;
16461653
const name = getComponentNameFromType(type);
16471654
const prevKeyPath = task.keyPath;
1648-
task.keyPath = [task.keyPath, name, key == null ? childIndex : key];
1655+
task.keyPath = [
1656+
task.keyPath,
1657+
name,
1658+
key == null ? (childIndex === -1 ? 0 : childIndex) : key,
1659+
];
16491660
renderElement(request, task, prevThenableState, type, props, ref);
16501661
task.keyPath = prevKeyPath;
16511662
return;
@@ -1805,61 +1816,29 @@ function renderChildrenArray(
18051816
children: Array<any>,
18061817
childIndex: number,
18071818
) {
1819+
const prevKeyPath = task.keyPath;
1820+
if (childIndex !== -1) {
1821+
task.keyPath = [task.keyPath, '', childIndex];
1822+
}
18081823
const prevTreeContext = task.treeContext;
18091824
const totalChildren = children.length;
18101825
for (let i = 0; i < totalChildren; i++) {
18111826
const node = children[i];
18121827
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i);
1813-
1814-
// Nested arrays behave like a "fragment node" which is keyed.
1815-
// Therefore we need to add the current index as a parent key.
1816-
// We first check if the nested nodes are arrays or iterables.
1817-
1818-
if (isArray(node)) {
1819-
const prevKeyPath = task.keyPath;
1820-
task.keyPath = [task.keyPath, '', childIndex];
1821-
renderChildrenArray(request, task, node, i);
1822-
task.keyPath = prevKeyPath;
1823-
continue;
1824-
}
1825-
1826-
const iteratorFn = getIteratorFn(node);
1827-
if (iteratorFn) {
1828-
if (__DEV__) {
1829-
validateIterable(node, iteratorFn);
1830-
}
1831-
const iterator = iteratorFn.call(node);
1832-
if (iterator) {
1833-
let step = iterator.next();
1834-
if (!step.done) {
1835-
const prevKeyPath = task.keyPath;
1836-
task.keyPath = [task.keyPath, '', childIndex];
1837-
const nestedChildren = [];
1838-
do {
1839-
nestedChildren.push(step.value);
1840-
step = iterator.next();
1841-
} while (!step.done);
1842-
renderChildrenArray(request, task, nestedChildren, i);
1843-
task.keyPath = prevKeyPath;
1844-
}
1845-
continue;
1846-
}
1847-
}
1848-
18491828
// We need to use the non-destructive form so that we can safely pop back
18501829
// up and render the sibling if something suspends.
18511830
renderNode(request, task, node, i);
18521831
}
18531832
// Because this context is always set right before rendering every child, we
18541833
// only need to reset it to the previous value at the very end.
18551834
task.treeContext = prevTreeContext;
1835+
task.keyPath = prevKeyPath;
18561836
}
18571837

18581838
function trackPostpone(
18591839
request: Request,
18601840
trackedPostpones: PostponedHoles,
18611841
task: Task,
1862-
childIndex: number,
18631842
segment: Segment,
18641843
): void {
18651844
segment.status = POSTPONED;
@@ -1901,8 +1880,20 @@ function trackPostpone(
19011880
);
19021881
}
19031882

1904-
const segmentNode: ResumableNode = [RESUME_SEGMENT, childIndex, segment.id];
1905-
addToResumableParent(segmentNode, keyPath, trackedPostpones);
1883+
if (task.childIndex === -1) {
1884+
// Resume at the position before the first array
1885+
const resumableElement = [
1886+
RESUME_ELEMENT,
1887+
keyPath[1],
1888+
keyPath[2],
1889+
segment.id,
1890+
];
1891+
addToResumableParent(resumableElement, keyPath[0], trackedPostpones);
1892+
} else {
1893+
// Resume at the slot within the array
1894+
const resumableNode = [RESUME_NODE, task.childIndex, segment.id];
1895+
addToResumableParent(resumableNode, keyPath, trackedPostpones);
1896+
}
19061897
}
19071898

19081899
function injectPostponedHole(
@@ -2060,13 +2051,7 @@ function renderNode(
20602051
task,
20612052
postponeInstance.message,
20622053
);
2063-
trackPostpone(
2064-
request,
2065-
trackedPostpones,
2066-
task,
2067-
childIndex,
2068-
postponedSegment,
2069-
);
2054+
trackPostpone(request, trackedPostpones, task, postponedSegment);
20702055

20712056
// Restore the context. We assume that this will be restored by the inner
20722057
// functions in case nothing throws so we don't use "finally" here.
@@ -2414,13 +2399,7 @@ function retryTask(request: Request, task: Task): void {
24142399
task.abortSet.delete(task);
24152400
const postponeInstance: Postpone = (x: any);
24162401
logPostpone(request, postponeInstance.message);
2417-
trackPostpone(
2418-
request,
2419-
trackedPostpones,
2420-
task,
2421-
task.childIndex,
2422-
segment,
2423-
);
2402+
trackPostpone(request, trackedPostpones, task, segment);
24242403
finishedTask(request, task.blockedBoundary, segment);
24252404
return;
24262405
}

0 commit comments

Comments
 (0)