Skip to content

Commit

Permalink
Postponing in the shell
Browse files Browse the repository at this point in the history
  • Loading branch information
sebmarkbage committed Oct 23, 2023
1 parent 6db7f42 commit 39d5b10
Show file tree
Hide file tree
Showing 3 changed files with 210 additions and 4 deletions.
23 changes: 23 additions & 0 deletions packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,29 @@ export function createResumableState(
};
}

export function resetResumableState(
resumableState: ResumableState,
renderState: RenderState,
): void {
// Resets the resumable state based on what didn't manage to fully flush in the render state.
// This currently assumes nothing was flushed.
resumableState.nextFormID = 0;
resumableState.hasBody = false;
resumableState.hasHtml = false;
resumableState.unknownResources = {};
resumableState.dnsResources = {};
resumableState.connectResources = {
default: {},
anonymous: {},
credentials: {},
};
resumableState.imageResources = {};
resumableState.styleResources = {};
resumableState.scriptResources = {};
resumableState.moduleUnknownResources = {};
resumableState.moduleScriptResources = {};
}

// Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion
// modes. We only include the variants as they matter for the sake of our purposes.
// We don't actually provide the namespace therefore we use constants instead of the string.
Expand Down
129 changes: 129 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1187,4 +1187,133 @@ describe('ReactDOMFizzStaticBrowser', () => {

expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
});

// @gate enablePostpone
it('emits an empty prelude and resumes at the root if we postpone in the shell', async () => {
let prerendering = true;
function Postpone() {
if (prerendering) {
React.unstable_postpone();
}
return 'Hello';
}

function App() {
return (
<html lang="en">
<body>
<link rel="stylesheet" href="my-style" precedence="high" />
<Postpone />
</body>
</html>
);
}

const prerendered = await ReactDOMFizzStatic.prerender(<App />);
expect(prerendered.postponed).not.toBe(null);

prerendering = false;

expect(await readContent(prerendered.prelude)).toBe('');

const content = await ReactDOMFizzServer.resume(
<App />,
JSON.parse(JSON.stringify(prerendered.postponed)),
);

expect(await readContent(content)).toBe(
'<!DOCTYPE html><html lang="en"><head>' +
'<link rel="stylesheet" href="my-style" data-precedence="high"/>' +
'</head><body>Hello</body></html>',
);
});

// @gate enablePostpone
it('emits an empty prelude if we have not rendered html or head tags yet', async () => {
let prerendering = true;
function Postpone() {
if (prerendering) {
React.unstable_postpone();
}
return (
<html lang="en">
<body>Hello</body>
</html>
);
}

function App() {
return (
<>
<link rel="stylesheet" href="my-style" precedence="high" />
<Postpone />
</>
);
}

const prerendered = await ReactDOMFizzStatic.prerender(<App />);
expect(prerendered.postponed).not.toBe(null);

prerendering = false;

expect(await readContent(prerendered.prelude)).toBe('');

const content = await ReactDOMFizzServer.resume(
<App />,
JSON.parse(JSON.stringify(prerendered.postponed)),
);

expect(await readContent(content)).toBe(
'<!DOCTYPE html><html lang="en"><head>' +
'<link rel="stylesheet" href="my-style" data-precedence="high"/>' +
'</head><body>Hello</body></html>',
);
});

// @gate enablePostpone
it('emits an empty prelude if a postpone in a promise in the shell', async () => {
let prerendering = true;
function Postpone() {
if (prerendering) {
React.unstable_postpone();
}
return 'Hello';
}

const Lazy = React.lazy(async () => {
await 0;
return {default: Postpone};
});

function App() {
return (
<html>
<link rel="stylesheet" href="my-style" precedence="high" />
<body>
<div>
<Lazy />
</div>
</body>
</html>
);
}

const prerendered = await ReactDOMFizzStatic.prerender(<App />);
expect(prerendered.postponed).not.toBe(null);

prerendering = false;

expect(await readContent(prerendered.prelude)).toBe('');

const content = await ReactDOMFizzServer.resume(
<App />,
JSON.parse(JSON.stringify(prerendered.postponed)),
);

expect(await readContent(content)).toBe(
'<!DOCTYPE html><html><head>' +
'<link rel="stylesheet" href="my-style" data-precedence="high"/>' +
'</head><body><div>Hello</div></body></html>',
);
});
});
62 changes: 58 additions & 4 deletions packages/react-server/src/ReactFizzServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import {
requestStorage,
pushFormStateMarkerIsMatching,
pushFormStateMarkerIsNotMatching,
resetResumableState,
} from './ReactFizzConfig';
import {
constructClassInstance,
Expand Down Expand Up @@ -505,6 +506,39 @@ export function resumeRequest(
onFatalError: onFatalError === undefined ? noop : onFatalError,
formState: null,
};
if (typeof postponedState.replaySlots === 'number') {
const resumedId = postponedState.replaySlots;
// We have a resume slot at the very root. This is effectively just a full rerender.
const rootSegment = createPendingSegment(
request,
0,
null,
postponedState.rootFormatContext,
// Root segments are never embedded in Text on either edge
false,
false,
);
rootSegment.id = resumedId;
// There is no parent so conceptually, we're unblocked to flush this segment.
rootSegment.parentFlushed = true;
const rootTask = createRenderTask(
request,
null,
children,
-1,
null,
rootSegment,
abortSet,
null,
postponedState.rootFormatContext,
emptyContextObject,
rootContextSnapshot,
emptyTreeContext,
);
pingedTasks.push(rootTask);
return request;
}

const replay: ReplaySet = {
nodes: postponedState.replayNodes,
slots: postponedState.replaySlots,
Expand Down Expand Up @@ -2477,6 +2511,17 @@ function trackPostpone(

const keyPath = task.keyPath;
const boundary = task.blockedBoundary;

if (boundary === null) {
segment.id = request.nextSegmentId++;
trackedPostpones.rootSlots = segment.id;
if (request.completedRootSegment !== null) {
// Postpone the root if this was a deeper segment.
request.completedRootSegment.status = POSTPONED;
}
return;
}

if (boundary !== null && boundary.status === PENDING) {
boundary.status = POSTPONED;
// We need to eagerly assign it an ID because we'll need to refer to
Expand Down Expand Up @@ -2835,7 +2880,7 @@ function renderNode(
enablePostpone &&
request.trackedPostpones !== null &&
x.$$typeof === REACT_POSTPONE_TYPE &&
task.blockedBoundary !== null // TODO: Support holes in the shell
task.blockedBoundary !== null // bubble if we're postponing in the shell
) {
// If we're tracking postpones, we inject a hole here and continue rendering
// sibling. Similar to suspending. If we're not tracking, we treat it more like
Expand Down Expand Up @@ -3376,8 +3421,7 @@ function retryRenderTask(
} else if (
enablePostpone &&
request.trackedPostpones !== null &&
x.$$typeof === REACT_POSTPONE_TYPE &&
task.blockedBoundary !== null // TODO: Support holes in the shell
x.$$typeof === REACT_POSTPONE_TYPE
) {
// If we're tracking postpones, we mark this segment as postponed and finish
// the task without filling it in. If we're not tracking, we treat it more like
Expand Down Expand Up @@ -3870,7 +3914,10 @@ function flushCompletedQueues(
let i;
const completedRootSegment = request.completedRootSegment;
if (completedRootSegment !== null) {
if (request.pendingRootTasks === 0) {
if (completedRootSegment.status === POSTPONED) {
// We postponed the root, so we write nothing.
return;
} else if (request.pendingRootTasks === 0) {
if (enableFloat) {
writePreamble(
destination,
Expand Down Expand Up @@ -4138,6 +4185,13 @@ export function getPostponedState(request: Request): null | PostponedState {
request.trackedPostpones = null;
return null;
}
if (
request.completedRootSegment !== null &&
request.completedRootSegment.status === POSTPONED
) {
// We postponed the root so we didn't flush anything.
resetResumableState(request.resumableState, request.renderState);
}
return {
nextSegmentId: request.nextSegmentId,
rootFormatContext: request.rootFormatContext,
Expand Down

0 comments on commit 39d5b10

Please sign in to comment.