Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -4087,6 +4087,28 @@ export function writePlaceholder(
return writeChunkAndReturn(destination, placeholder2);
}

// Activity boundaries are encoded as comments.
const startActivityBoundary = stringToPrecomputedChunk('<!--&-->');
const endActivityBoundary = stringToPrecomputedChunk('<!--/&-->');

export function pushStartActivityBoundary(
target: Array<Chunk | PrecomputedChunk>,
renderState: RenderState,
): void {
target.push(startActivityBoundary);
}

export function pushEndActivityBoundary(
target: Array<Chunk | PrecomputedChunk>,
renderState: RenderState,
preambleState: null | PreambleState,
): void {
if (preambleState) {
pushPreambleContribution(target, preambleState);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I believe I still need to emit the preamble contribution for these since the Activity boundary can fail to hydrate and switch to client rendering. I'm not quite sure if this preambleState in the render phase is correct at this timing. I guess so?

Copy link
Collaborator

Choose a reason for hiding this comment

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

The preambleState here is for the Suspense boundary. by writing it into the activity boundary we need to know the the preamble contribution was actually part of this activity boundary not the containing Suspense boundary. I would have expected to see blockedPreamble updated when rendering an activity boundary not pushing the marker into the activity chunks.

We should only include the marker here if the preamble parts were actually inside the boundary which means we need to treat it as having it's own preamble state separate from the Suspense boundary it is contained within

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 was fixed by #32850 since the preamblestate is no longer need to track the contribution and it's just part of the output.

}
target.push(endActivityBoundary);
}

// Suspense boundaries are encoded as comments.
const startCompletedSuspenseBoundary = stringToPrecomputedChunk('<!--$-->');
const startPendingSuspenseBoundary1 = stringToPrecomputedChunk(
Expand Down Expand Up @@ -4225,6 +4247,23 @@ export function writeEndClientRenderedSuspenseBoundary(
const boundaryPreambleContributionChunkStart = stringToPrecomputedChunk('<!--');
const boundaryPreambleContributionChunkEnd = stringToPrecomputedChunk('-->');

function pushPreambleContribution(
target: Array<Chunk | PrecomputedChunk>,
preambleState: PreambleState,
) {
// Same as writePreambleContribution but for the render phase.
const contribution = preambleState.contribution;
if (contribution !== NoContribution) {
target.push(
boundaryPreambleContributionChunkStart,
// This is a number type so we can do the fast path without coercion checking
// eslint-disable-next-line react-internal/safe-string-coercion
stringToChunk('' + contribution),
boundaryPreambleContributionChunkEnd,
);
}
}

function writePreambleContribution(
destination: Destination,
preambleState: PreambleState,
Expand Down
25 changes: 25 additions & 0 deletions packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
createRenderState as createRenderStateImpl,
pushTextInstance as pushTextInstanceImpl,
pushSegmentFinale as pushSegmentFinaleImpl,
pushStartActivityBoundary as pushStartActivityBoundaryImpl,
pushEndActivityBoundary as pushEndActivityBoundaryImpl,
writeStartCompletedSuspenseBoundary as writeStartCompletedSuspenseBoundaryImpl,
writeStartClientRenderedSuspenseBoundary as writeStartClientRenderedSuspenseBoundaryImpl,
writeEndCompletedSuspenseBoundary as writeEndCompletedSuspenseBoundaryImpl,
Expand Down Expand Up @@ -207,6 +209,29 @@ export function pushSegmentFinale(
}
}

export function pushStartActivityBoundary(
target: Array<Chunk | PrecomputedChunk>,
renderState: RenderState,
): void {
if (renderState.generateStaticMarkup) {
// A completed boundary is done and doesn't need a representation in the HTML
// if we're not going to be hydrating it.
return;
}
pushStartActivityBoundaryImpl(target, renderState);
}

export function pushEndActivityBoundary(
target: Array<Chunk | PrecomputedChunk>,
renderState: RenderState,
preambleState: null | PreambleState,
): void {
if (renderState.generateStaticMarkup) {
return;
}
pushEndActivityBoundaryImpl(target, renderState, preambleState);
}

export function writeStartCompletedSuspenseBoundary(
destination: Destination,
renderState: RenderState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3669,7 +3669,7 @@ describe('ReactDOMServerPartialHydration', () => {
});

// @gate enableActivity
it('a visible Activity component acts like a fragment', async () => {
it('a visible Activity component is surrounded by comment markers', async () => {
const ref = React.createRef();

function App() {
Expand All @@ -3690,9 +3690,11 @@ describe('ReactDOMServerPartialHydration', () => {
// pure indirection.
expect(container).toMatchInlineSnapshot(`
<div>
<!--&-->
<span>
Child
</span>
<!--/&-->
</div>
`);

Expand Down Expand Up @@ -3739,6 +3741,8 @@ describe('ReactDOMServerPartialHydration', () => {
<span>
Visible
</span>
<!--&-->
<!--/&-->
</div>
`);

Expand All @@ -3760,6 +3764,8 @@ describe('ReactDOMServerPartialHydration', () => {
<span>
Visible
</span>
<!--&-->
<!--/&-->
<span
style="display: none;"
>
Expand Down
18 changes: 18 additions & 0 deletions packages/react-markup/src/ReactFizzConfigMarkup.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,13 +151,31 @@ export function pushSegmentFinale(
return;
}

export function pushStartActivityBoundary(
target: Array<Chunk | PrecomputedChunk>,
renderState: RenderState,
): void {
// Markup doesn't have any instructions.
return;
}

export function pushEndActivityBoundary(
target: Array<Chunk | PrecomputedChunk>,
renderState: RenderState,
preambleState: null | PreambleState,
): void {
// Markup doesn't have any instructions.
return;
}

export function writeStartCompletedSuspenseBoundary(
destination: Destination,
renderState: RenderState,
): boolean {
// Markup doesn't have any instructions.
return true;
}

export function writeStartClientRenderedSuspenseBoundary(
destination: Destination,
renderState: RenderState,
Expand Down
51 changes: 45 additions & 6 deletions packages/react-noop-renderer/src/ReactNoopServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ type TextInstance = {
hidden: boolean,
};

type ActivityInstance = {
children: Array<Instance | TextInstance | SuspenseInstance>,
};

type SuspenseInstance = {
state: 'pending' | 'complete' | 'client-render',
children: Array<Instance | TextInstance | SuspenseInstance>,
Expand Down Expand Up @@ -164,44 +168,74 @@ const ReactNoopServer = ReactFizzServer({
});
},

pushStartActivityBoundary(
target: Array<Uint8Array>,
renderState: RenderState,
): void {
const activityInstance: ActivityInstance = {
children: [],
};
target.push(Buffer.from(JSON.stringify(activityInstance), 'utf8'));
},

pushEndActivityBoundary(
target: Array<Uint8Array>,
renderState: RenderState,
preambleState: null | PreambleState,
): void {
target.push(POP);
},

writeStartCompletedSuspenseBoundary(
destination: Destination,
renderState: RenderState,
suspenseInstance: SuspenseInstance,
): boolean {
suspenseInstance.state = 'complete';
const suspenseInstance: SuspenseInstance = {
state: 'complete',
children: [],
};
const parent = destination.stack[destination.stack.length - 1];
parent.children.push(suspenseInstance);
destination.stack.push(suspenseInstance);
return true;
},
writeStartPendingSuspenseBoundary(
destination: Destination,
renderState: RenderState,
suspenseInstance: SuspenseInstance,
): boolean {
suspenseInstance.state = 'pending';
const suspenseInstance: SuspenseInstance = {
state: 'pending',
children: [],
};
const parent = destination.stack[destination.stack.length - 1];
parent.children.push(suspenseInstance);
destination.stack.push(suspenseInstance);
return true;
},
writeStartClientRenderedSuspenseBoundary(
destination: Destination,
renderState: RenderState,
suspenseInstance: SuspenseInstance,
): boolean {
suspenseInstance.state = 'client-render';
const suspenseInstance: SuspenseInstance = {
state: 'client-render',
children: [],
};
const parent = destination.stack[destination.stack.length - 1];
parent.children.push(suspenseInstance);
destination.stack.push(suspenseInstance);
return true;
},
writeEndCompletedSuspenseBoundary(destination: Destination): boolean {
destination.stack.pop();
return true;
},
writeEndPendingSuspenseBoundary(destination: Destination): boolean {
destination.stack.pop();
return true;
},
writeEndClientRenderedSuspenseBoundary(destination: Destination): boolean {
destination.stack.pop();
return true;
},

writeStartSegment(
Expand All @@ -218,9 +252,11 @@ const ReactNoopServer = ReactFizzServer({
throw new Error('Segments are only expected at the root of the stack.');
}
destination.stack.push(segment);
return true;
},
writeEndSegment(destination: Destination, formatContext: null): boolean {
destination.stack.pop();
return true;
},

writeCompletedSegmentInstruction(
Expand All @@ -241,6 +277,7 @@ const ReactNoopServer = ReactFizzServer({
0,
...segment.children,
);
return true;
},

writeCompletedBoundaryInstruction(
Expand All @@ -255,6 +292,7 @@ const ReactNoopServer = ReactFizzServer({
}
boundary.children = segment.children;
boundary.state = 'complete';
return true;
},

writeClientRenderBoundaryInstruction(
Expand All @@ -263,6 +301,7 @@ const ReactNoopServer = ReactFizzServer({
boundary: SuspenseInstance,
): boolean {
boundary.status = 'client-render';
return true;
},

writePreambleStart() {},
Expand Down
53 changes: 41 additions & 12 deletions packages/react-server/src/ReactFizzServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ import {
import {
writeCompletedRoot,
writePlaceholder,
pushStartActivityBoundary,
pushEndActivityBoundary,
writeStartCompletedSuspenseBoundary,
writeStartPendingSuspenseBoundary,
writeStartClientRenderedSuspenseBoundary,
Expand Down Expand Up @@ -2200,23 +2202,50 @@ function renderLazyComponent(
renderElement(request, task, keyPath, Component, resolvedProps, ref);
}

function renderOffscreen(
function renderActivity(
request: Request,
task: Task,
keyPath: KeyNode,
props: Object,
): void {
const mode: ?OffscreenMode = (props.mode: any);
if (mode === 'hidden') {
// A hidden Offscreen boundary is not server rendered. Prerendering happens
// on the client.
const segment = task.blockedSegment;
if (segment === null) {
// Replay
const mode: ?OffscreenMode = (props.mode: any);
if (mode === 'hidden') {
// A hidden Activity boundary is not server rendered. Prerendering happens
// on the client.
} else {
// A visible Activity boundary has its children rendered inside the boundary.
const prevKeyPath = task.keyPath;
task.keyPath = keyPath;
renderNode(request, task, props.children, -1);
task.keyPath = prevKeyPath;
}
} else {
// A visible Offscreen boundary is treated exactly like a fragment: a
// pure indirection.
const prevKeyPath = task.keyPath;
task.keyPath = keyPath;
renderNodeDestructive(request, task, props.children, -1);
task.keyPath = prevKeyPath;
// Render
// An Activity boundary is delimited so that we can hydrate it separately.
pushStartActivityBoundary(segment.chunks, request.renderState);
segment.lastPushedText = false;
const mode: ?OffscreenMode = (props.mode: any);
if (mode === 'hidden') {
// A hidden Activity boundary is not server rendered. Prerendering happens
// on the client.
} else {
// A visible Activity boundary has its children rendered inside the boundary.
const prevKeyPath = task.keyPath;
task.keyPath = keyPath;
// We use the non-destructive form because if something suspends, we still
// need to pop back up and finish the end comment.
renderNode(request, task, props.children, -1);
task.keyPath = prevKeyPath;
}
pushEndActivityBoundary(
segment.chunks,
request.renderState,
task.blockedPreamble,
);
segment.lastPushedText = false;
}
}

Expand Down Expand Up @@ -2291,7 +2320,7 @@ function renderElement(
return;
}
case REACT_ACTIVITY_TYPE: {
renderOffscreen(request, task, keyPath, props);
renderActivity(request, task, keyPath, props);
return;
}
case REACT_SUSPENSE_LIST_TYPE: {
Expand Down
2 changes: 2 additions & 0 deletions packages/react-server/src/forks/ReactFizzConfig.custom.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export const pushFormStateMarkerIsNotMatching =
$$$config.pushFormStateMarkerIsNotMatching;
export const writeCompletedRoot = $$$config.writeCompletedRoot;
export const writePlaceholder = $$$config.writePlaceholder;
export const pushStartActivityBoundary = $$$config.pushStartActivityBoundary;
export const pushEndActivityBoundary = $$$config.pushEndActivityBoundary;
export const writeStartCompletedSuspenseBoundary =
$$$config.writeStartCompletedSuspenseBoundary;
export const writeStartPendingSuspenseBoundary =
Expand Down
Loading