Skip to content

Commit 758a018

Browse files
committed
Postponing in the shell
1 parent 6db7f42 commit 758a018

File tree

6 files changed

+214
-4
lines changed

6 files changed

+214
-4
lines changed

packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,29 @@ export function createResumableState(
576576
};
577577
}
578578

579+
export function resetResumableState(
580+
resumableState: ResumableState,
581+
renderState: RenderState,
582+
): void {
583+
// Resets the resumable state based on what didn't manage to fully flush in the render state.
584+
// This currently assumes nothing was flushed.
585+
resumableState.nextFormID = 0;
586+
resumableState.hasBody = false;
587+
resumableState.hasHtml = false;
588+
resumableState.unknownResources = {};
589+
resumableState.dnsResources = {};
590+
resumableState.connectResources = {
591+
default: {},
592+
anonymous: {},
593+
credentials: {},
594+
};
595+
resumableState.imageResources = {};
596+
resumableState.styleResources = {};
597+
resumableState.scriptResources = {};
598+
resumableState.moduleUnknownResources = {};
599+
resumableState.moduleScriptResources = {};
600+
}
601+
579602
// Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion
580603
// modes. We only include the variants as they matter for the sake of our purposes.
581604
// We don't actually provide the namespace therefore we use constants instead of the string.

packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ export {
158158
hoistResources,
159159
setCurrentlyRenderingBoundaryResourcesTarget,
160160
prepareHostDispatcher,
161+
resetResumableState,
161162
} from './ReactFizzConfigDOM';
162163

163164
import escapeTextForBrowser from './escapeTextForBrowser';

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

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1187,4 +1187,133 @@ describe('ReactDOMFizzStaticBrowser', () => {
11871187

11881188
expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
11891189
});
1190+
1191+
// @gate enablePostpone
1192+
it('emits an empty prelude and resumes at the root if we postpone in the shell', async () => {
1193+
let prerendering = true;
1194+
function Postpone() {
1195+
if (prerendering) {
1196+
React.unstable_postpone();
1197+
}
1198+
return 'Hello';
1199+
}
1200+
1201+
function App() {
1202+
return (
1203+
<html lang="en">
1204+
<body>
1205+
<link rel="stylesheet" href="my-style" precedence="high" />
1206+
<Postpone />
1207+
</body>
1208+
</html>
1209+
);
1210+
}
1211+
1212+
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
1213+
expect(prerendered.postponed).not.toBe(null);
1214+
1215+
prerendering = false;
1216+
1217+
expect(await readContent(prerendered.prelude)).toBe('');
1218+
1219+
const content = await ReactDOMFizzServer.resume(
1220+
<App />,
1221+
JSON.parse(JSON.stringify(prerendered.postponed)),
1222+
);
1223+
1224+
expect(await readContent(content)).toBe(
1225+
'<!DOCTYPE html><html lang="en"><head>' +
1226+
'<link rel="stylesheet" href="my-style" data-precedence="high"/>' +
1227+
'</head><body>Hello</body></html>',
1228+
);
1229+
});
1230+
1231+
// @gate enablePostpone
1232+
it('emits an empty prelude if we have not rendered html or head tags yet', async () => {
1233+
let prerendering = true;
1234+
function Postpone() {
1235+
if (prerendering) {
1236+
React.unstable_postpone();
1237+
}
1238+
return (
1239+
<html lang="en">
1240+
<body>Hello</body>
1241+
</html>
1242+
);
1243+
}
1244+
1245+
function App() {
1246+
return (
1247+
<>
1248+
<link rel="stylesheet" href="my-style" precedence="high" />
1249+
<Postpone />
1250+
</>
1251+
);
1252+
}
1253+
1254+
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
1255+
expect(prerendered.postponed).not.toBe(null);
1256+
1257+
prerendering = false;
1258+
1259+
expect(await readContent(prerendered.prelude)).toBe('');
1260+
1261+
const content = await ReactDOMFizzServer.resume(
1262+
<App />,
1263+
JSON.parse(JSON.stringify(prerendered.postponed)),
1264+
);
1265+
1266+
expect(await readContent(content)).toBe(
1267+
'<!DOCTYPE html><html lang="en"><head>' +
1268+
'<link rel="stylesheet" href="my-style" data-precedence="high"/>' +
1269+
'</head><body>Hello</body></html>',
1270+
);
1271+
});
1272+
1273+
// @gate enablePostpone
1274+
it('emits an empty prelude if a postpone in a promise in the shell', async () => {
1275+
let prerendering = true;
1276+
function Postpone() {
1277+
if (prerendering) {
1278+
React.unstable_postpone();
1279+
}
1280+
return 'Hello';
1281+
}
1282+
1283+
const Lazy = React.lazy(async () => {
1284+
await 0;
1285+
return {default: Postpone};
1286+
});
1287+
1288+
function App() {
1289+
return (
1290+
<html>
1291+
<link rel="stylesheet" href="my-style" precedence="high" />
1292+
<body>
1293+
<div>
1294+
<Lazy />
1295+
</div>
1296+
</body>
1297+
</html>
1298+
);
1299+
}
1300+
1301+
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
1302+
expect(prerendered.postponed).not.toBe(null);
1303+
1304+
prerendering = false;
1305+
1306+
expect(await readContent(prerendered.prelude)).toBe('');
1307+
1308+
const content = await ReactDOMFizzServer.resume(
1309+
<App />,
1310+
JSON.parse(JSON.stringify(prerendered.postponed)),
1311+
);
1312+
1313+
expect(await readContent(content)).toBe(
1314+
'<!DOCTYPE html><html><head>' +
1315+
'<link rel="stylesheet" href="my-style" data-precedence="high"/>' +
1316+
'</head><body><div>Hello</div></body></html>',
1317+
);
1318+
});
11901319
});

packages/react-noop-renderer/src/ReactNoopServer.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ const ReactNoopServer = ReactFizzServer({
9494
return null;
9595
},
9696

97+
resetResumableState(): void {},
98+
9799
pushTextInstance(
98100
target: Array<Uint8Array>,
99101
text: string,

packages/react-server/src/ReactFizzServer.js

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ import {
7676
requestStorage,
7777
pushFormStateMarkerIsMatching,
7878
pushFormStateMarkerIsNotMatching,
79+
resetResumableState,
7980
} from './ReactFizzConfig';
8081
import {
8182
constructClassInstance,
@@ -505,6 +506,39 @@ export function resumeRequest(
505506
onFatalError: onFatalError === undefined ? noop : onFatalError,
506507
formState: null,
507508
};
509+
if (typeof postponedState.replaySlots === 'number') {
510+
const resumedId = postponedState.replaySlots;
511+
// We have a resume slot at the very root. This is effectively just a full rerender.
512+
const rootSegment = createPendingSegment(
513+
request,
514+
0,
515+
null,
516+
postponedState.rootFormatContext,
517+
// Root segments are never embedded in Text on either edge
518+
false,
519+
false,
520+
);
521+
rootSegment.id = resumedId;
522+
// There is no parent so conceptually, we're unblocked to flush this segment.
523+
rootSegment.parentFlushed = true;
524+
const rootTask = createRenderTask(
525+
request,
526+
null,
527+
children,
528+
-1,
529+
null,
530+
rootSegment,
531+
abortSet,
532+
null,
533+
postponedState.rootFormatContext,
534+
emptyContextObject,
535+
rootContextSnapshot,
536+
emptyTreeContext,
537+
);
538+
pingedTasks.push(rootTask);
539+
return request;
540+
}
541+
508542
const replay: ReplaySet = {
509543
nodes: postponedState.replayNodes,
510544
slots: postponedState.replaySlots,
@@ -2477,6 +2511,17 @@ function trackPostpone(
24772511

24782512
const keyPath = task.keyPath;
24792513
const boundary = task.blockedBoundary;
2514+
2515+
if (boundary === null) {
2516+
segment.id = request.nextSegmentId++;
2517+
trackedPostpones.rootSlots = segment.id;
2518+
if (request.completedRootSegment !== null) {
2519+
// Postpone the root if this was a deeper segment.
2520+
request.completedRootSegment.status = POSTPONED;
2521+
}
2522+
return;
2523+
}
2524+
24802525
if (boundary !== null && boundary.status === PENDING) {
24812526
boundary.status = POSTPONED;
24822527
// We need to eagerly assign it an ID because we'll need to refer to
@@ -2835,7 +2880,7 @@ function renderNode(
28352880
enablePostpone &&
28362881
request.trackedPostpones !== null &&
28372882
x.$$typeof === REACT_POSTPONE_TYPE &&
2838-
task.blockedBoundary !== null // TODO: Support holes in the shell
2883+
task.blockedBoundary !== null // bubble if we're postponing in the shell
28392884
) {
28402885
// If we're tracking postpones, we inject a hole here and continue rendering
28412886
// sibling. Similar to suspending. If we're not tracking, we treat it more like
@@ -3376,8 +3421,7 @@ function retryRenderTask(
33763421
} else if (
33773422
enablePostpone &&
33783423
request.trackedPostpones !== null &&
3379-
x.$$typeof === REACT_POSTPONE_TYPE &&
3380-
task.blockedBoundary !== null // TODO: Support holes in the shell
3424+
x.$$typeof === REACT_POSTPONE_TYPE
33813425
) {
33823426
// If we're tracking postpones, we mark this segment as postponed and finish
33833427
// the task without filling it in. If we're not tracking, we treat it more like
@@ -3870,7 +3914,10 @@ function flushCompletedQueues(
38703914
let i;
38713915
const completedRootSegment = request.completedRootSegment;
38723916
if (completedRootSegment !== null) {
3873-
if (request.pendingRootTasks === 0) {
3917+
if (completedRootSegment.status === POSTPONED) {
3918+
// We postponed the root, so we write nothing.
3919+
return;
3920+
} else if (request.pendingRootTasks === 0) {
38743921
if (enableFloat) {
38753922
writePreamble(
38763923
destination,
@@ -4138,6 +4185,13 @@ export function getPostponedState(request: Request): null | PostponedState {
41384185
request.trackedPostpones = null;
41394186
return null;
41404187
}
4188+
if (
4189+
request.completedRootSegment !== null &&
4190+
request.completedRootSegment.status === POSTPONED
4191+
) {
4192+
// We postponed the root so we didn't flush anything.
4193+
resetResumableState(request.resumableState, request.renderState);
4194+
}
41414195
return {
41424196
nextSegmentId: request.nextSegmentId,
41434197
rootFormatContext: request.rootFormatContext,

packages/react-server/src/forks/ReactFizzConfig.custom.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export const isPrimaryRenderer = false;
3939
export const supportsRequestStorage = false;
4040
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
4141

42+
export const resetResumableState = $$$config.resetResumableState;
4243
export const getChildFormatContext = $$$config.getChildFormatContext;
4344
export const makeId = $$$config.makeId;
4445
export const pushTextInstance = $$$config.pushTextInstance;

0 commit comments

Comments
 (0)