Skip to content

Commit

Permalink
Track the key path difference between right before the first array an…
Browse files Browse the repository at this point in the history
…d 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.
  • Loading branch information
sebmarkbage committed Sep 12, 2023
1 parent bb1d8d1 commit 7e018e7
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ describe('ReactDOMFizzStaticBrowser', () => {
if (prerendering) {
React.unstable_postpone();
}
return 'Hello';
return ['Hello', 'World'];
}

function App() {
Expand Down
119 changes: 49 additions & 70 deletions packages/react-server/src/ReactFizzServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,8 @@ type KeyNode = [

const REPLAY_NODE = 0;
const REPLAY_SUSPENSE_BOUNDARY = 1;
const RESUME_SEGMENT = 2;
const RESUME_NODE = 2;
const RESUME_ELEMENT = 3;

type ResumableParentNode =
| [
Expand All @@ -185,9 +186,15 @@ type ResumableParentNode =
type ResumableNode =
| ResumableParentNode
| [
2, // RESUME_SEGMENT
2, // RESUME_NODE
number /* index */,
number /* segment id */,
]
| [
3, // RESUME_ELEMENT
string | null /* name */,
string | number /* key */,
number /* segment id */,
];

type PostponedHoles = {
Expand Down Expand Up @@ -784,7 +791,7 @@ function renderSuspenseBoundary(
}
try {
// We use the safe form because we don't handle suspending here. Only error handling.
renderNode(request, task, content, 0);
renderNode(request, task, content, -1);
pushSegmentFinale(
contentRootSegment.chunks,
request.renderState,
Expand Down Expand Up @@ -873,7 +880,7 @@ function renderBackupSuspenseBoundary(
const segment = task.blockedSegment;

pushStartCompletedSuspenseBoundary(segment.chunks);
renderNode(request, task, content, 0);
renderNode(request, task, content, -1);
pushEndCompletedSuspenseBoundary(segment.chunks);

popComponentStackInDEV(task);
Expand Down Expand Up @@ -903,7 +910,7 @@ function renderHostElement(

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

// We expect that errors will fatal the whole task and that we don't need
// the correct context. Therefore this is not in a finally.
Expand Down Expand Up @@ -970,13 +977,13 @@ function finishClassComponent(
childContextTypes,
);
task.legacyContext = mergedContext;
renderNodeDestructive(request, task, null, nextChildren, 0);
renderNodeDestructive(request, task, null, nextChildren, -1);
task.legacyContext = previousContext;
return;
}
}

renderNodeDestructive(request, task, null, nextChildren, 0);
renderNodeDestructive(request, task, null, nextChildren, -1);
}

function renderClassComponent(
Expand Down Expand Up @@ -1170,20 +1177,20 @@ function finishFunctionComponent(
// Modify the id context. Because we'll need to reset this if something
// suspends or errors, we'll use the non-destructive render path.
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index);
renderNode(request, task, children, 0);
renderNode(request, task, children, -1);
// Like the other contexts, this does not need to be in a finally block
// because renderNode takes care of unwinding the stack.
task.treeContext = prevTreeContext;
} else if (didEmitFormStateMarkers) {
// If there were formState hooks, we must use the non-destructive path
// because this component is not a pure indirection; we emitted markers
// to the stream.
renderNode(request, task, children, 0);
renderNode(request, task, children, -1);
} else {
// We're now successfully past this task, and we haven't modified the
// context stack. We don't have to pop back to the previous task every
// again, so we can use the destructive recursive form.
renderNodeDestructive(request, task, null, children, 0);
renderNodeDestructive(request, task, null, children, -1);
}
}

Expand Down Expand Up @@ -1353,7 +1360,7 @@ function renderContextConsumer(
const newValue = readContext(context);
const newChildren = render(newValue);

renderNodeDestructive(request, task, null, newChildren, 0);
renderNodeDestructive(request, task, null, newChildren, -1);
}

function renderContextProvider(
Expand All @@ -1370,7 +1377,7 @@ function renderContextProvider(
prevSnapshot = task.context;
}
task.context = pushProvider(context, value);
renderNodeDestructive(request, task, null, children, 0);
renderNodeDestructive(request, task, null, children, -1);
task.context = popProvider(context);
if (__DEV__) {
if (prevSnapshot !== task.context) {
Expand Down Expand Up @@ -1413,7 +1420,7 @@ function renderOffscreen(request: Request, task: Task, props: Object): void {
} else {
// A visible Offscreen boundary is treated exactly like a fragment: a
// pure indirection.
renderNodeDestructive(request, task, null, props.children, 0);
renderNodeDestructive(request, task, null, props.children, -1);
}
}

Expand Down Expand Up @@ -1460,7 +1467,7 @@ function renderElement(
case REACT_STRICT_MODE_TYPE:
case REACT_PROFILER_TYPE:
case REACT_FRAGMENT_TYPE: {
renderNodeDestructive(request, task, null, props.children, 0);
renderNodeDestructive(request, task, null, props.children, -1);
return;
}
case REACT_OFFSCREEN_TYPE: {
Expand All @@ -1470,13 +1477,13 @@ function renderElement(
case REACT_SUSPENSE_LIST_TYPE: {
pushBuiltInComponentStackInDEV(task, 'SuspenseList');
// TODO: SuspenseList should control the boundaries.
renderNodeDestructive(request, task, null, props.children, 0);
renderNodeDestructive(request, task, null, props.children, -1);
popComponentStackInDEV(task);
return;
}
case REACT_SCOPE_TYPE: {
if (enableScopeAPI) {
renderNodeDestructive(request, task, null, props.children, 0);
renderNodeDestructive(request, task, null, props.children, -1);
return;
}
throw new Error('ReactDOMServer does not yet support scope components.');
Expand Down Expand Up @@ -1645,7 +1652,11 @@ function renderNodeDestructiveImpl(
const ref = element.ref;
const name = getComponentNameFromType(type);
const prevKeyPath = task.keyPath;
task.keyPath = [task.keyPath, name, key == null ? childIndex : key];
task.keyPath = [
task.keyPath,
name,
key == null ? (childIndex === -1 ? 0 : childIndex) : key,
];
renderElement(request, task, prevThenableState, type, props, ref);
task.keyPath = prevKeyPath;
return;
Expand Down Expand Up @@ -1805,61 +1816,29 @@ function renderChildrenArray(
children: Array<any>,
childIndex: number,
) {
const prevKeyPath = task.keyPath;
if (childIndex !== -1) {
task.keyPath = [task.keyPath, '', childIndex];
}
const prevTreeContext = task.treeContext;
const totalChildren = children.length;
for (let i = 0; i < totalChildren; i++) {
const node = children[i];
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i);

// Nested arrays behave like a "fragment node" which is keyed.
// Therefore we need to add the current index as a parent key.
// We first check if the nested nodes are arrays or iterables.

if (isArray(node)) {
const prevKeyPath = task.keyPath;
task.keyPath = [task.keyPath, '', childIndex];
renderChildrenArray(request, task, node, i);
task.keyPath = prevKeyPath;
continue;
}

const iteratorFn = getIteratorFn(node);
if (iteratorFn) {
if (__DEV__) {
validateIterable(node, iteratorFn);
}
const iterator = iteratorFn.call(node);
if (iterator) {
let step = iterator.next();
if (!step.done) {
const prevKeyPath = task.keyPath;
task.keyPath = [task.keyPath, '', childIndex];
const nestedChildren = [];
do {
nestedChildren.push(step.value);
step = iterator.next();
} while (!step.done);
renderChildrenArray(request, task, nestedChildren, i);
task.keyPath = prevKeyPath;
}
continue;
}
}

// We need to use the non-destructive form so that we can safely pop back
// up and render the sibling if something suspends.
renderNode(request, task, node, i);
}
// Because this context is always set right before rendering every child, we
// only need to reset it to the previous value at the very end.
task.treeContext = prevTreeContext;
task.keyPath = prevKeyPath;
}

function trackPostpone(
request: Request,
trackedPostpones: PostponedHoles,
task: Task,
childIndex: number,
segment: Segment,
): void {
segment.status = POSTPONED;
Expand Down Expand Up @@ -1901,8 +1880,20 @@ function trackPostpone(
);
}

const segmentNode: ResumableNode = [RESUME_SEGMENT, childIndex, segment.id];
addToResumableParent(segmentNode, keyPath, trackedPostpones);
if (task.childIndex === -1) {
// Resume at the position before the first array
const resumableElement = [
RESUME_ELEMENT,
keyPath[1],
keyPath[2],
segment.id,
];
addToResumableParent(resumableElement, keyPath[0], trackedPostpones);
} else {
// Resume at the slot within the array
const resumableNode = [RESUME_NODE, task.childIndex, segment.id];
addToResumableParent(resumableNode, keyPath, trackedPostpones);
}
}

function injectPostponedHole(
Expand Down Expand Up @@ -2060,13 +2051,7 @@ function renderNode(
task,
postponeInstance.message,
);
trackPostpone(
request,
trackedPostpones,
task,
childIndex,
postponedSegment,
);
trackPostpone(request, trackedPostpones, task, postponedSegment);

// Restore the context. We assume that this will be restored by the inner
// functions in case nothing throws so we don't use "finally" here.
Expand Down Expand Up @@ -2414,13 +2399,7 @@ function retryTask(request: Request, task: Task): void {
task.abortSet.delete(task);
const postponeInstance: Postpone = (x: any);
logPostpone(request, postponeInstance.message);
trackPostpone(
request,
trackedPostpones,
task,
task.childIndex,
segment,
);
trackPostpone(request, trackedPostpones, task, segment);
finishedTask(request, task.blockedBoundary, segment);
return;
}
Expand Down

0 comments on commit 7e018e7

Please sign in to comment.