Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fizz] Track Key Path #27243

Merged
merged 1 commit into from Aug 18, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
99 changes: 75 additions & 24 deletions packages/react-server/src/ReactFizzServer.js
Expand Up @@ -151,6 +151,13 @@ const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
const ReactCurrentCache = ReactSharedInternals.ReactCurrentCache;
const ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;

// Linked list representing the identity of a component given the component/tag name and key.
// The name might be minified but we assume that it's going to be the same generated name. Typically
// because it's just the same compiled output in practice.
type KeyNode =
| null
| [KeyNode /* parent */, string | null /* name */, string | number /* key */];

type LegacyContext = {
[key: string]: any,
};
Expand All @@ -176,6 +183,7 @@ export type Task = {
blockedBoundary: Root | SuspenseBoundary,
blockedSegment: Segment, // the segment we'll write to
abortSet: Set<Task>, // the abortable set that this task belongs to
keyPath: KeyNode, // the path of all parent keys currently rendering
legacyContext: LegacyContext, // the current legacy context that this task is executing in
context: ContextSnapshot, // the current new context that this task is executing in
treeContext: TreeContext, // the current tree context that this task is executing in
Expand Down Expand Up @@ -335,6 +343,7 @@ export function createRequest(
null,
rootSegment,
abortSet,
null,
emptyContextObject,
rootContextSnapshot,
emptyTreeContext,
Expand Down Expand Up @@ -388,6 +397,7 @@ function createTask(
blockedBoundary: Root | SuspenseBoundary,
blockedSegment: Segment,
abortSet: Set<Task>,
keyPath: KeyNode,
legacyContext: LegacyContext,
context: ContextSnapshot,
treeContext: TreeContext,
Expand All @@ -404,6 +414,7 @@ function createTask(
blockedBoundary,
blockedSegment,
abortSet,
keyPath,
legacyContext,
context,
treeContext,
Expand Down Expand Up @@ -617,7 +628,7 @@ function renderSuspenseBoundary(
}
try {
// We use the safe form because we don't handle suspending here. Only error handling.
renderNode(request, task, content);
renderNode(request, task, content, 0);
pushSegmentFinale(
contentRootSegment.chunks,
request.responseState,
Expand Down Expand Up @@ -678,6 +689,7 @@ function renderSuspenseBoundary(
parentBoundary,
boundarySegment,
fallbackAbortSet,
task.keyPath,
task.legacyContext,
task.context,
task.treeContext,
Expand All @@ -703,7 +715,7 @@ function renderBackupSuspenseBoundary(
const segment = task.blockedSegment;

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

popComponentStackInDEV(task);
Expand Down Expand Up @@ -733,7 +745,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);
renderNode(request, task, children, 0);

// 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 @@ -800,13 +812,13 @@ function finishClassComponent(
childContextTypes,
);
task.legacyContext = mergedContext;
renderNodeDestructive(request, task, null, nextChildren);
renderNodeDestructive(request, task, null, nextChildren, 0);
task.legacyContext = previousContext;
return;
}
}

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

function renderClassComponent(
Expand Down Expand Up @@ -957,12 +969,12 @@ function renderIndeterminateComponent(
const index = 0;
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index);
try {
renderNodeDestructive(request, task, null, value);
renderNodeDestructive(request, task, null, value, 0);
} finally {
task.treeContext = prevTreeContext;
}
} else {
renderNodeDestructive(request, task, null, value);
renderNodeDestructive(request, task, null, value, 0);
}
}
popComponentStackInDEV(task);
Expand Down Expand Up @@ -1062,12 +1074,12 @@ function renderForwardRef(
const index = 0;
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index);
try {
renderNodeDestructive(request, task, null, children);
renderNodeDestructive(request, task, null, children, 0);
} finally {
task.treeContext = prevTreeContext;
}
} else {
renderNodeDestructive(request, task, null, children);
renderNodeDestructive(request, task, null, children, 0);
}
popComponentStackInDEV(task);
}
Expand Down Expand Up @@ -1139,7 +1151,7 @@ function renderContextConsumer(
const newValue = readContext(context);
const newChildren = render(newValue);

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

function renderContextProvider(
Expand All @@ -1156,7 +1168,7 @@ function renderContextProvider(
prevSnapshot = task.context;
}
task.context = pushProvider(context, value);
renderNodeDestructive(request, task, null, children);
renderNodeDestructive(request, task, null, children, 0);
task.context = popProvider(context);
if (__DEV__) {
if (prevSnapshot !== task.context) {
Expand Down Expand Up @@ -1199,7 +1211,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);
renderNodeDestructive(request, task, null, props.children, 0);
}
}

Expand Down Expand Up @@ -1246,7 +1258,7 @@ function renderElement(
case REACT_STRICT_MODE_TYPE:
case REACT_PROFILER_TYPE:
case REACT_FRAGMENT_TYPE: {
renderNodeDestructive(request, task, null, props.children);
renderNodeDestructive(request, task, null, props.children, 0);
return;
}
case REACT_OFFSCREEN_TYPE: {
Expand All @@ -1256,13 +1268,13 @@ function renderElement(
case REACT_SUSPENSE_LIST_TYPE: {
pushBuiltInComponentStackInDEV(task, 'SuspenseList');
// TODO: SuspenseList should control the boundaries.
renderNodeDestructive(request, task, null, props.children);
renderNodeDestructive(request, task, null, props.children, 0);
popComponentStackInDEV(task);
return;
}
case REACT_SCOPE_TYPE: {
if (enableScopeAPI) {
renderNodeDestructive(request, task, null, props.children);
renderNodeDestructive(request, task, null, props.children, 0);
return;
}
throw new Error('ReactDOMServer does not yet support scope components.');
Expand Down Expand Up @@ -1368,13 +1380,20 @@ function renderNodeDestructive(
// always null, except when called by retryTask.
prevThenableState: ThenableState | null,
node: ReactNodeList,
childIndex: number,
): void {
if (__DEV__) {
// In Dev we wrap renderNodeDestructiveImpl in a try / catch so we can capture
// a component stack at the right place in the tree. We don't do this in renderNode
// becuase it is not called at every layer of the tree and we may lose frames
try {
return renderNodeDestructiveImpl(request, task, prevThenableState, node);
return renderNodeDestructiveImpl(
request,
task,
prevThenableState,
node,
childIndex,
);
} catch (x) {
if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
// This is a Wakable, noop
Expand All @@ -1389,7 +1408,13 @@ function renderNodeDestructive(
throw x;
}
} else {
return renderNodeDestructiveImpl(request, task, prevThenableState, node);
return renderNodeDestructiveImpl(
request,
task,
prevThenableState,
node,
childIndex,
);
}
}

Expand All @@ -1400,6 +1425,7 @@ function renderNodeDestructiveImpl(
task: Task,
prevThenableState: ThenableState | null,
node: ReactNodeList,
childIndex: number,
): void {
// Stash the node we're working on. We'll pick up from this task in case
// something suspends.
Expand All @@ -1411,9 +1437,14 @@ function renderNodeDestructiveImpl(
case REACT_ELEMENT_TYPE: {
const element: React$Element<any> = (node: any);
const type = element.type;
const key = element.key;
const props = element.props;
const ref = element.ref;
const name = getComponentNameFromType(type);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're going to go check the type again in renderElement, we could move this code deeper and extract the name in each slot but there's so much code duplication I stayed away from that even though that's my usual M.O.

const prevKeyPath = task.keyPath;
task.keyPath = [task.keyPath, name, key == null ? childIndex : key];
renderElement(request, task, prevThenableState, type, props, ref);
task.keyPath = prevKeyPath;
return;
}
case REACT_PORTAL_TYPE:
Expand Down Expand Up @@ -1446,13 +1477,13 @@ function renderNodeDestructiveImpl(
} else {
resolvedNode = init(payload);
}
renderNodeDestructive(request, task, null, resolvedNode);
renderNodeDestructive(request, task, null, resolvedNode, childIndex);
return;
}
}

if (isArray(node)) {
renderChildrenArray(request, task, node);
renderChildrenArray(request, task, node, childIndex);
return;
}

Expand All @@ -1476,7 +1507,7 @@ function renderNodeDestructiveImpl(
children.push(step.value);
step = iterator.next();
} while (!step.done);
renderChildrenArray(request, task, children);
renderChildrenArray(request, task, children, childIndex);
return;
}
return;
Expand All @@ -1500,6 +1531,7 @@ function renderNodeDestructiveImpl(
task,
null,
unwrapThenable(thenable),
childIndex,
);
}

Expand All @@ -1513,6 +1545,7 @@ function renderNodeDestructiveImpl(
task,
null,
readContext(context),
childIndex,
);
}

Expand Down Expand Up @@ -1567,17 +1600,26 @@ function renderChildrenArray(
request: Request,
task: Task,
children: Array<any>,
childIndex: number,
) {
const prevKeyPath = task.keyPath;
const totalChildren = children.length;
for (let i = 0; i < totalChildren; i++) {
const prevTreeContext = task.treeContext;
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i);
try {
const node = children[i];
if (isArray(node) || getIteratorFn(node)) {
// Nested arrays behave like a "fragment node" which is keyed.
// Therefore we need to add the current index as a parent key.
task.keyPath = [task.keyPath, '', childIndex];
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is subtle but the first array inside of a component is the same as having a single element with slot 0. So the array itself doesn't take up slot 0. In other words, we the outer most array doesn't get added to the path but the next one gets treated as if it was a fragment element in that slot.

}
// 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, children[i]);
renderNode(request, task, node, i);
} finally {
task.treeContext = prevTreeContext;
task.keyPath = prevKeyPath;
}
}
}
Expand Down Expand Up @@ -1611,6 +1653,7 @@ function spawnNewSuspendedTask(
task.blockedBoundary,
newSegment,
task.abortSet,
task.keyPath,
task.legacyContext,
task.context,
task.treeContext,
Expand All @@ -1629,7 +1672,12 @@ function spawnNewSuspendedTask(

// This is a non-destructive form of rendering a node. If it suspends it spawns
// a new task and restores the context of this task to what it was before.
function renderNode(request: Request, task: Task, node: ReactNodeList): void {
function renderNode(
request: Request,
task: Task,
node: ReactNodeList,
childIndex: number,
): void {
// Store how much we've pushed at this point so we can reset it in case something
// suspended partially through writing something.
const segment = task.blockedSegment;
Expand All @@ -1641,12 +1689,13 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
const previousFormatContext = task.blockedSegment.formatContext;
const previousLegacyContext = task.legacyContext;
const previousContext = task.context;
const previousKeyPath = task.keyPath;
let previousComponentStack = null;
if (__DEV__) {
previousComponentStack = task.componentStack;
}
try {
return renderNodeDestructive(request, task, null, node);
return renderNodeDestructive(request, task, null, node, childIndex);
} catch (thrownValue) {
resetHooksState();

Expand Down Expand Up @@ -1675,6 +1724,7 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
task.blockedSegment.formatContext = previousFormatContext;
task.legacyContext = previousLegacyContext;
task.context = previousContext;
task.keyPath = previousKeyPath;
// Restore all active ReactContexts to what they were before.
switchContext(previousContext);
if (__DEV__) {
Expand All @@ -1687,6 +1737,7 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
task.blockedSegment.formatContext = previousFormatContext;
task.legacyContext = previousLegacyContext;
task.context = previousContext;
task.keyPath = previousKeyPath;
// Restore all active ReactContexts to what they were before.
switchContext(previousContext);
if (__DEV__) {
Expand Down Expand Up @@ -1955,7 +2006,7 @@ function retryTask(request: Request, task: Task): void {
const prevThenableState = task.thenableState;
task.thenableState = null;

renderNodeDestructive(request, task, prevThenableState, task.node);
renderNodeDestructive(request, task, prevThenableState, task.node, 0);
pushSegmentFinale(
segment.chunks,
request.responseState,
Expand Down