@samitouri / QOS-React / commits / e10e868182

Schedule prerender after something suspends (#30800)

Adds the concept of a "prerender". These special renders are spawned whenever something suspends (and we're not already prerendering). The purpose is to move speculative rendering work into a separate phase that does not block the UI from updating. For example, during a transition, if something suspends, we should not speculatively prerender siblings that will be replaced by a fallback in the UI until *after* the fallback has been shown to the user.

Andrew Clark committed Sep 4, 2024 at 13:55 UTC e10e8681824e56c10fdb14e0359d878bcd748937
24 files changed +1084 -135
packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js
+19 -6
@@ -148,7 +148,15 @@ describe('ReactCache', () => {
148 error = e;
149 }
150 expect(error.message).toMatch('Failed to load: Hi');
151 - assertLog(['Promise rejected [Hi]', 'Error! [Hi]', 'Error! [Hi]']);
151 + assertLog([
152 + 'Promise rejected [Hi]',
153 + 'Error! [Hi]',
154 + 'Error! [Hi]',
155 +
156 + ...(gate('enableSiblingPrerendering')
157 + ? ['Error! [Hi]', 'Error! [Hi]']
158 + : []),
159 + ]);
160
161 // Should throw again on a subsequent read
162 root.render(<App />);
@@ -191,6 +199,7 @@ describe('ReactCache', () => {
199 }
200 });
201
202 + // @gate enableSiblingPrerendering
203 it('evicts least recently used values', async () => {
204 ReactCache.unstable_setGlobalCacheLimit(3);
205
@@ -206,15 +215,13 @@ describe('ReactCache', () => {
215 await waitForAll(['Suspend! [1]', 'Loading...']);
216 jest.advanceTimersByTime(100);
217 assertLog(['Promise resolved [1]']);
209 - await waitForAll([1, 'Suspend! [2]']);
218 + await waitForAll([1, 'Suspend! [2]', 1, 'Suspend! [2]', 'Suspend! [3]']);
219
220 jest.advanceTimersByTime(100);
212 - assertLog(['Promise resolved [2]']);
213 - await waitForAll([1, 2, 'Suspend! [3]']);
221 + assertLog(['Promise resolved [2]', 'Promise resolved [3]']);
222 + await waitForAll([1, 2, 3]);
223
224 await act(() => jest.advanceTimersByTime(100));
216 - assertLog(['Promise resolved [3]', 1, 2, 3]);
217 -
225 expect(root).toMatchRenderedOutput('123');
226
227 // Render 1, 4, 5
@@ -234,6 +241,9 @@ describe('ReactCache', () => {
241 1,
242 4,
243 'Suspend! [5]',
244 + 1,
245 + 4,
246 + 'Suspend! [5]',
247 'Promise resolved [5]',
248 1,
249 4,
@@ -267,6 +277,9 @@ describe('ReactCache', () => {
277 1,
278 2,
279 'Suspend! [3]',
280 + 1,
281 + 2,
282 + 'Suspend! [3]',
283 'Promise resolved [3]',
284 1,
285 2,
packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js
+1 -1
@@ -744,7 +744,7 @@ describe('ReactDOMFiberAsync', () => {
744 // Because it suspended, it remains on the current path
745 expect(div.textContent).toBe('/path/a');
746 });
747 - assertLog([]);
747 + assertLog(gate('enableSiblingPrerendering') ? ['Suspend! [/path/b]'] : []);
748
749 await act(async () => {
750 resolvePromise();
packages/react-dom/src/__tests__/ReactDOMForm-test.js
+54 -7
@@ -699,7 +699,15 @@ describe('ReactDOMForm', () => {
699 // This should suspend because form actions are implicitly wrapped
700 // in startTransition.
701 await submit(formRef.current);
702 - assertLog(['Pending...', 'Suspend! [Updated]', 'Loading...']);
702 + assertLog([
703 + 'Pending...',
704 + 'Suspend! [Updated]',
705 + 'Loading...',
706 +
707 + ...(gate('enableSiblingPrerendering')
708 + ? ['Suspend! [Updated]', 'Loading...']
709 + : []),
710 + ]);
711 expect(container.textContent).toBe('Pending...Initial');
712
713 await act(() => resolveText('Updated'));
@@ -736,7 +744,15 @@ describe('ReactDOMForm', () => {
744
745 // Update
746 await submit(formRef.current);
739 - assertLog(['Pending...', 'Suspend! [Count: 1]', 'Loading...']);
747 + assertLog([
748 + 'Pending...',
749 + 'Suspend! [Count: 1]',
750 + 'Loading...',
751 +
752 + ...(gate('enableSiblingPrerendering')
753 + ? ['Suspend! [Count: 1]', 'Loading...']
754 + : []),
755 + ]);
756 expect(container.textContent).toBe('Pending...Count: 0');
757
758 await act(() => resolveText('Count: 1'));
@@ -745,7 +761,15 @@ describe('ReactDOMForm', () => {
761
762 // Update again
763 await submit(formRef.current);
748 - assertLog(['Pending...', 'Suspend! [Count: 2]', 'Loading...']);
764 + assertLog([
765 + 'Pending...',
766 + 'Suspend! [Count: 2]',
767 + 'Loading...',
768 +
769 + ...(gate('enableSiblingPrerendering')
770 + ? ['Suspend! [Count: 2]', 'Loading...']
771 + : []),
772 + ]);
773 expect(container.textContent).toBe('Pending...Count: 1');
774
775 await act(() => resolveText('Count: 2'));
@@ -789,7 +813,14 @@ describe('ReactDOMForm', () => {
813 assertLog(['Async action started', 'Pending...']);
814
815 await act(() => resolveText('Wait'));
792 - assertLog(['Suspend! [Updated]', 'Loading...']);
816 + assertLog([
817 + 'Suspend! [Updated]',
818 + 'Loading...',
819 +
820 + ...(gate('enableSiblingPrerendering')
821 + ? ['Suspend! [Updated]', 'Loading...']
822 + : []),
823 + ]);
824 expect(container.textContent).toBe('Pending...Initial');
825
826 await act(() => resolveText('Updated'));
@@ -1475,7 +1506,15 @@ describe('ReactDOMForm', () => {
1506 // Now dispatch inside of a transition. This one does not trigger a
1507 // loading state.
1508 await act(() => startTransition(() => dispatch()));
1478 - assertLog(['Count: 1', 'Suspend! [Count: 2]', 'Loading...']);
1509 + assertLog([
1510 + 'Count: 1',
1511 + 'Suspend! [Count: 2]',
1512 + 'Loading...',
1513 +
1514 + ...(gate('enableSiblingPrerendering')
1515 + ? ['Suspend! [Count: 2]', 'Loading...']
1516 + : []),
1517 + ]);
1518 expect(container.textContent).toBe('Count: 1');
1519
1520 await act(() => resolveText('Count: 2'));
@@ -1495,7 +1534,11 @@ describe('ReactDOMForm', () => {
1534
1535 const root = ReactDOMClient.createRoot(container);
1536 await act(() => root.render(<App />));
1498 - assertLog(['Suspend! [Count: 0]']);
1537 + assertLog([
1538 + 'Suspend! [Count: 0]',
1539 +
1540 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [Count: 0]'] : []),
1541 + ]);
1542 await act(() => resolveText('Count: 0'));
1543 assertLog(['Count: 0']);
1544
@@ -1508,7 +1551,11 @@ describe('ReactDOMForm', () => {
1551 {withoutStack: true},
1552 ],
1553 ]);
1511 - assertLog(['Suspend! [Count: 1]']);
1554 + assertLog([
1555 + 'Suspend! [Count: 1]',
1556 +
1557 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [Count: 1]'] : []),
1558 + ]);
1559 expect(container.textContent).toBe('Count: 0');
1560 });
1561
packages/react-reconciler/src/ReactFiberLane.js
+50
@@ -229,28 +229,49 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
229
230 const suspendedLanes = root.suspendedLanes;
231 const pingedLanes = root.pingedLanes;
232 + const warmLanes = root.warmLanes;
233
234 // Do not work on any idle work until all the non-idle work has finished,
235 // even if the work is suspended.
236 const nonIdlePendingLanes = pendingLanes & NonIdleLanes;
237 if (nonIdlePendingLanes !== NoLanes) {
238 + // First check for fresh updates.
239 const nonIdleUnblockedLanes = nonIdlePendingLanes & ~suspendedLanes;
240 if (nonIdleUnblockedLanes !== NoLanes) {
241 nextLanes = getHighestPriorityLanes(nonIdleUnblockedLanes);
242 } else {
243 + // No fresh updates. Check if suspended work has been pinged.
244 const nonIdlePingedLanes = nonIdlePendingLanes & pingedLanes;
245 if (nonIdlePingedLanes !== NoLanes) {
246 nextLanes = getHighestPriorityLanes(nonIdlePingedLanes);
247 + } else {
248 + // Nothing has been pinged. Check for lanes that need to be prewarmed.
249 + const lanesToPrewarm = nonIdlePendingLanes & ~warmLanes;
250 + if (lanesToPrewarm !== NoLanes) {
251 + nextLanes = getHighestPriorityLanes(lanesToPrewarm);
252 + }
253 }
254 }
255 } else {
256 // The only remaining work is Idle.
257 + // TODO: Idle isn't really used anywhere, and the thinking around
258 + // speculative rendering has evolved since this was implemented. Consider
259 + // removing until we've thought about this again.
260 +
261 + // First check for fresh updates.
262 const unblockedLanes = pendingLanes & ~suspendedLanes;
263 if (unblockedLanes !== NoLanes) {
264 nextLanes = getHighestPriorityLanes(unblockedLanes);
265 } else {
266 + // No fresh updates. Check if suspended work has been pinged.
267 if (pingedLanes !== NoLanes) {
268 nextLanes = getHighestPriorityLanes(pingedLanes);
269 + } else {
270 + // Nothing has been pinged. Check for lanes that need to be prewarmed.
271 + const lanesToPrewarm = pendingLanes & ~warmLanes;
272 + if (lanesToPrewarm !== NoLanes) {
273 + nextLanes = getHighestPriorityLanes(lanesToPrewarm);
274 + }
275 }
276 }
277 }
@@ -335,6 +356,21 @@ export function getNextLanesToFlushSync(
356 return NoLanes;
357 }
358
359 +export function checkIfRootIsPrerendering(
360 + root: FiberRoot,
361 + renderLanes: Lanes,
362 +): boolean {
363 + const pendingLanes = root.pendingLanes;
364 + const suspendedLanes = root.suspendedLanes;
365 + const pingedLanes = root.pingedLanes;
366 + // Remove lanes that are suspended (but not pinged)
367 + const unblockedLanes = pendingLanes & ~(suspendedLanes & ~pingedLanes);
368 +
369 + // If there are no unsuspended or pinged lanes, that implies that we're
370 + // performing a prerender.
371 + return (unblockedLanes & renderLanes) === 0;
372 +}
373 +
374 export function getEntangledLanes(root: FiberRoot, renderLanes: Lanes): Lanes {
375 let entangledLanes = renderLanes;
376
@@ -670,6 +706,7 @@ export function markRootUpdated(root: FiberRoot, updateLane: Lane) {
706 if (updateLane !== IdleLane) {
707 root.suspendedLanes = NoLanes;
708 root.pingedLanes = NoLanes;
709 + root.warmLanes = NoLanes;
710 }
711 }
712
@@ -677,10 +714,19 @@ export function markRootSuspended(
714 root: FiberRoot,
715 suspendedLanes: Lanes,
716 spawnedLane: Lane,
717 + didSkipSuspendedSiblings: boolean,
718 ) {
719 root.suspendedLanes |= suspendedLanes;
720 root.pingedLanes &= ~suspendedLanes;
721
722 + if (!didSkipSuspendedSiblings) {
723 + // Mark these lanes as warm so we know there's nothing else to work on.
724 + root.warmLanes |= suspendedLanes;
725 + } else {
726 + // Render unwound without attempting all the siblings. Do no mark the lanes
727 + // as warm. This will cause a prewarm render to be scheduled.
728 + }
729 +
730 // The suspended lanes are no longer CPU-bound. Clear their expiration times.
731 const expirationTimes = root.expirationTimes;
732 let lanes = suspendedLanes;
@@ -700,6 +746,9 @@ export function markRootSuspended(
746
747 export function markRootPinged(root: FiberRoot, pingedLanes: Lanes) {
748 root.pingedLanes |= root.suspendedLanes & pingedLanes;
749 + // The data that just resolved could have unblocked additional children, which
750 + // will also need to be prewarmed if something suspends again.
751 + root.warmLanes &= ~pingedLanes;
752 }
753
754 export function markRootFinished(
@@ -714,6 +763,7 @@ export function markRootFinished(
763 // Let's try everything again
764 root.suspendedLanes = NoLanes;
765 root.pingedLanes = NoLanes;
766 + root.warmLanes = NoLanes;
767
768 root.expiredLanes &= remainingLanes;
769
packages/react-reconciler/src/ReactFiberRoot.js
+1
@@ -75,6 +75,7 @@ function FiberRootNode(
75 this.pendingLanes = NoLanes;
76 this.suspendedLanes = NoLanes;
77 this.pingedLanes = NoLanes;
78 + this.warmLanes = NoLanes;
79 this.expiredLanes = NoLanes;
80 this.finishedLanes = NoLanes;
81 this.errorRecoveryDisabledLanes = NoLanes;
packages/react-reconciler/src/ReactFiberWorkLoop.js
+100 -22
@@ -161,6 +161,7 @@ import {
161 SyncUpdateLanes,
162 UpdateLanes,
163 claimNextTransitionLane,
164 + checkIfRootIsPrerendering,
165 } from './ReactFiberLane';
166 import {
167 DiscreteEventPriority,
@@ -329,6 +330,15 @@ const SuspendedOnHydration: SuspendedReason = 8;
330 let workInProgressSuspendedReason: SuspendedReason = NotSuspended;
331 let workInProgressThrownValue: mixed = null;
332
333 +// Tracks whether any siblings were skipped during the unwind phase after
334 +// something suspends. Used to determine whether to schedule another render
335 +// to prewarm the skipped siblings.
336 +let workInProgressRootDidSkipSuspendedSiblings: boolean = false;
337 +// Whether the work-in-progress render is the result of a prewarm/prerender.
338 +// This tells us whether or not we should render the siblings after
339 +// something suspends.
340 +let workInProgressRootIsPrerendering: boolean = false;
341 +
342 // Whether a ping listener was attached during this render. This is slightly
343 // different that whether something suspended, because we don't add multiple
344 // listeners to a promise we've already seen (per root and lane).
@@ -731,6 +741,7 @@ export function scheduleUpdateOnFiber(
741 root,
742 workInProgressRootRenderLanes,
743 workInProgressDeferredLane,
744 + workInProgressRootDidSkipSuspendedSiblings,
745 );
746 }
747
@@ -797,6 +808,7 @@ export function scheduleUpdateOnFiber(
808 root,
809 workInProgressRootRenderLanes,
810 workInProgressDeferredLane,
811 + workInProgressRootDidSkipSuspendedSiblings,
812 );
813 }
814 }
@@ -909,7 +921,12 @@ export function performConcurrentWorkOnRoot(
921 // The render unwound without completing the tree. This happens in special
922 // cases where need to exit the current render without producing a
923 // consistent tree or committing.
912 - markRootSuspended(root, lanes, NoLane);
924 + markRootSuspended(
925 + root,
926 + lanes,
927 + NoLane,
928 + workInProgressRootDidSkipSuspendedSiblings,
929 + );
930 } else {
931 // The render completed.
932
@@ -965,7 +982,12 @@ export function performConcurrentWorkOnRoot(
982 }
983 if (exitStatus === RootFatalErrored) {
984 prepareFreshStack(root, NoLanes);
968 - markRootSuspended(root, lanes, NoLane);
985 + markRootSuspended(
986 + root,
987 + lanes,
988 + NoLane,
989 + workInProgressRootDidSkipSuspendedSiblings,
990 + );
991 break;
992 }
993
@@ -1088,7 +1110,12 @@ function finishConcurrentRender(
1110 // This is a transition, so we should exit without committing a
1111 // placeholder and without scheduling a timeout. Delay indefinitely
1112 // until we receive more data.
1091 - markRootSuspended(root, lanes, workInProgressDeferredLane);
1113 + markRootSuspended(
1114 + root,
1115 + lanes,
1116 + workInProgressDeferredLane,
1117 + workInProgressRootDidSkipSuspendedSiblings,
1118 + );
1119 return;
1120 }
1121 // Commit the placeholder.
@@ -1132,7 +1159,12 @@ function finishConcurrentRender(
1159
1160 // Don't bother with a very short suspense time.
1161 if (msUntilTimeout > 10) {
1135 - markRootSuspended(root, lanes, workInProgressDeferredLane);
1162 + markRootSuspended(
1163 + root,
1164 + lanes,
1165 + workInProgressDeferredLane,
1166 + workInProgressRootDidSkipSuspendedSiblings,
1167 + );
1168
1169 const nextLanes = getNextLanes(root, NoLanes);
1170 if (nextLanes !== NoLanes) {
@@ -1156,6 +1188,7 @@ function finishConcurrentRender(
1188 workInProgressRootDidIncludeRecursiveRenderUpdate,
1189 lanes,
1190 workInProgressDeferredLane,
1191 + workInProgressRootDidSkipSuspendedSiblings,
1192 ),
1193 msUntilTimeout,
1194 );
@@ -1170,6 +1203,7 @@ function finishConcurrentRender(
1203 workInProgressRootDidIncludeRecursiveRenderUpdate,
1204 lanes,
1205 workInProgressDeferredLane,
1206 + workInProgressRootDidSkipSuspendedSiblings,
1207 );
1208 }
1209 }
@@ -1182,6 +1216,7 @@ function commitRootWhenReady(
1216 didIncludeRenderPhaseUpdate: boolean,
1217 lanes: Lanes,
1218 spawnedLane: Lane,
1219 + didSkipSuspendedSiblings: boolean,
1220 ) {
1221 // TODO: Combine retry throttling with Suspensey commits. Right now they run
1222 // one after the other.
@@ -1221,7 +1256,7 @@ function commitRootWhenReady(
1256 didIncludeRenderPhaseUpdate,
1257 ),
1258 );
1224 - markRootSuspended(root, lanes, spawnedLane);
1259 + markRootSuspended(root, lanes, spawnedLane, didSkipSuspendedSiblings);
1260 return;
1261 }
1262 }
@@ -1333,6 +1368,7 @@ function markRootSuspended(
1368 root: FiberRoot,
1369 suspendedLanes: Lanes,
1370 spawnedLane: Lane,
1371 + didSkipSuspendedSiblings: boolean,
1372 ) {
1373 // When suspending, we should always exclude lanes that were pinged or (more
1374 // rarely, since we try to avoid it) updated during the render phase.
@@ -1341,7 +1377,12 @@ function markRootSuspended(
1377 suspendedLanes,
1378 workInProgressRootInterleavedUpdatedLanes,
1379 );
1344 - _markRootSuspended(root, suspendedLanes, spawnedLane);
1380 + _markRootSuspended(
1381 + root,
1382 + suspendedLanes,
1383 + spawnedLane,
1384 + didSkipSuspendedSiblings,
1385 + );
1386 }
1387
1388 // This is the entry point for synchronous tasks that don't go
@@ -1393,7 +1434,7 @@ export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null {
1434
1435 if (exitStatus === RootFatalErrored) {
1436 prepareFreshStack(root, NoLanes);
1396 - markRootSuspended(root, lanes, NoLane);
1437 + markRootSuspended(root, lanes, NoLane, false);
1438 ensureRootIsScheduled(root);
1439 return null;
1440 }
@@ -1402,7 +1443,12 @@ export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null {
1443 // The render unwound without completing the tree. This happens in special
1444 // cases where need to exit the current render without producing a
1445 // consistent tree or committing.
1405 - markRootSuspended(root, lanes, workInProgressDeferredLane);
1446 + markRootSuspended(
1447 + root,
1448 + lanes,
1449 + workInProgressDeferredLane,
1450 + workInProgressRootDidSkipSuspendedSiblings,
1451 + );
1452 ensureRootIsScheduled(root);
1453 return null;
1454 }
@@ -1636,6 +1682,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1682 workInProgressRootRenderLanes = lanes;
1683 workInProgressSuspendedReason = NotSuspended;
1684 workInProgressThrownValue = null;
1685 + workInProgressRootDidSkipSuspendedSiblings = false;
1686 + workInProgressRootIsPrerendering = checkIfRootIsPrerendering(root, lanes);
1687 workInProgressRootDidAttachPingListener = false;
1688 workInProgressRootExitStatus = RootInProgress;
1689 workInProgressRootSkippedLanes = NoLanes;
@@ -1940,6 +1988,7 @@ export function renderDidSuspendDelayIfPossible(): void {
1988 workInProgressRoot,
1989 workInProgressRootRenderLanes,
1990 workInProgressDeferredLane,
1991 + workInProgressRootDidSkipSuspendedSiblings,
1992 );
1993 }
1994 }
@@ -2589,7 +2638,30 @@ function throwAndUnwindWorkLoop(
2638
2639 if (unitOfWork.flags & Incomplete) {
2640 // Unwind the stack until we reach the nearest boundary.
2592 - unwindUnitOfWork(unitOfWork);
2641 + let skipSiblings;
2642 + if (!enableSiblingPrerendering) {
2643 + skipSiblings = true;
2644 + } else {
2645 + if (
2646 + // The current algorithm for both hydration and error handling assumes
2647 + // that the tree is rendered sequentially. So we always skip the siblings.
2648 + getIsHydrating() ||
2649 + workInProgressSuspendedReason === SuspendedOnError
2650 + ) {
2651 + skipSiblings = true;
2652 + // We intentionally don't set workInProgressRootDidSkipSuspendedSiblings,
2653 + // because we don't want to trigger another prerender attempt.
2654 + } else if (!workInProgressRootIsPrerendering) {
2655 + // This is not a prerender. Skip the siblings during this render. A
2656 + // separate prerender will be scheduled for later.
2657 + skipSiblings = true;
2658 + workInProgressRootDidSkipSuspendedSiblings = true;
2659 + } else {
2660 + // This is a prerender. Don't skip the siblings.
2661 + skipSiblings = false;
2662 + }
2663 + }
2664 + unwindUnitOfWork(unitOfWork, skipSiblings);
2665 } else {
2666 // Although the fiber suspended, we're intentionally going to commit it in
2667 // an inconsistent state. We can do this safely in cases where we know the
@@ -2625,15 +2697,16 @@ function completeUnitOfWork(unitOfWork: Fiber): void {
2697 // sibling. If there are no more siblings, return to the parent fiber.
2698 let completedWork: Fiber = unitOfWork;
2699 do {
2628 - if (__DEV__) {
2629 - if ((completedWork.flags & Incomplete) !== NoFlags) {
2630 - // NOTE: If we re-enable sibling prerendering in some cases, this branch
2631 - // is where we would switch to the unwinding path.
2632 - console.error(
2633 - 'Internal React error: Expected this fiber to be complete, but ' +
2634 - "it isn't. It should have been unwound. This is a bug in React.",
2635 - );
2636 - }
2700 + if ((completedWork.flags & Incomplete) !== NoFlags) {
2701 + // This fiber did not complete, because one of its children did not
2702 + // complete. Switch to unwinding the stack instead of completing it.
2703 + //
2704 + // The reason "unwind" and "complete" is interleaved is because when
2705 + // something suspends, we continue rendering the siblings even though
2706 + // they will be replaced by a fallback.
2707 + const skipSiblings = workInProgressRootDidSkipSuspendedSiblings;
2708 + unwindUnitOfWork(completedWork, skipSiblings);
2709 + return;
2710 }
2711
2712 // The current, flushed, state of this fiber is the alternate. Ideally
@@ -2697,7 +2770,7 @@ function completeUnitOfWork(unitOfWork: Fiber): void {
2770 }
2771 }
2772
2700 -function unwindUnitOfWork(unitOfWork: Fiber): void {
2773 +function unwindUnitOfWork(unitOfWork: Fiber, skipSiblings: boolean): void {
2774 let incompleteWork: Fiber = unitOfWork;
2775 do {
2776 // The current, flushed, state of this fiber is the alternate. Ideally
@@ -2754,9 +2827,14 @@ function unwindUnitOfWork(unitOfWork: Fiber): void {
2827 returnFiber.deletions = null;
2828 }
2829
2757 - // NOTE: If we re-enable sibling prerendering in some cases, here we
2758 - // would switch to the normal completion path: check if a sibling
2759 - // exists, and if so, begin work on it.
2830 + if (!skipSiblings) {
2831 + const siblingFiber = incompleteWork.sibling;
2832 + if (siblingFiber !== null) {
2833 + // This branch will return us to the normal work loop.
2834 + workInProgress = siblingFiber;
2835 + return;
2836 + }
2837 + }
2838
2839 // Otherwise, return to the parent
2840 // $FlowFixMe[incompatible-type] we bail out when we get a null
packages/react-reconciler/src/ReactInternalTypes.js
+1
@@ -256,6 +256,7 @@ type BaseFiberRootProperties = {
256 pendingLanes: Lanes,
257 suspendedLanes: Lanes,
258 pingedLanes: Lanes,
259 + warmLanes: Lanes,
260 expiredLanes: Lanes,
261 errorRecoveryDisabledLanes: Lanes,
262 shellSuspendCounter: number,
packages/react-reconciler/src/__tests__/ActivitySuspense-test.js
+18 -2
@@ -215,7 +215,15 @@ describe('Activity Suspense', () => {
215 );
216 });
217 });
218 - assertLog(['Open', 'Suspend! [Async]', 'Loading...']);
218 + assertLog([
219 + 'Open',
220 + 'Suspend! [Async]',
221 + 'Loading...',
222 +
223 + ...(gate('enableSiblingPrerendering')
224 + ? ['Open', 'Suspend! [Async]', 'Loading...']
225 + : []),
226 + ]);
227 // It should suspend with delay to prevent the already-visible Suspense
228 // boundary from switching to a fallback
229 expect(root).toMatchRenderedOutput(<span>Closed</span>);
@@ -276,7 +284,15 @@ describe('Activity Suspense', () => {
284 );
285 });
286 });
279 - assertLog(['Open', 'Suspend! [Async]', 'Loading...']);
287 + assertLog([
288 + 'Open',
289 + 'Suspend! [Async]',
290 + 'Loading...',
291 +
292 + ...(gate('enableSiblingPrerendering')
293 + ? ['Open', 'Suspend! [Async]', 'Loading...']
294 + : []),
295 + ]);
296 // It should suspend with delay to prevent the already-visible Suspense
297 // boundary from switching to a fallback
298 expect(root).toMatchRenderedOutput(
packages/react-reconciler/src/__tests__/ReactActWarnings-test.js
+8 -1
@@ -349,7 +349,14 @@ describe('act warnings', () => {
349 root.render(<App showMore={true} />);
350 });
351 });
352 - assertLog(['Suspend! [Async]', 'Loading...']);
352 + assertLog([
353 + 'Suspend! [Async]',
354 + 'Loading...',
355 +
356 + ...(gate('enableSiblingPrerendering')
357 + ? ['Suspend! [Async]', 'Loading...']
358 + : []),
359 + ]);
360 expect(root).toMatchRenderedOutput('(empty)');
361
362 // This is a ping, not a retry, because no fallback is showing.
packages/react-reconciler/src/__tests__/ReactAsyncActions-test.js
+32 -3
@@ -297,7 +297,15 @@ describe('ReactAsyncActions', () => {
297 // This will schedule an update on C, and also the async action scope
298 // will end. This will allow React to attempt to render the updates.
299 await act(() => resolveText('Wait before updating C'));
300 - assertLog(['Async action ended', 'Pending: false', 'Suspend! [A1]']);
300 + assertLog([
301 + 'Async action ended',
302 + 'Pending: false',
303 + 'Suspend! [A1]',
304 +
305 + ...(gate('enableSiblingPrerendering')
306 + ? ['Pending: false', 'Suspend! [A1]', 'Suspend! [B1]', 'Suspend! [C1]']
307 + : []),
308 + ]);
309 expect(root).toMatchRenderedOutput(
310 <>
311 <span>Pending: true</span>
@@ -309,7 +317,15 @@ describe('ReactAsyncActions', () => {
317 // together, only when the all of A, B, and C updates are unblocked is the
318 // render allowed to proceed.
319 await act(() => resolveText('A1'));
312 - assertLog(['Pending: false', 'A1', 'Suspend! [B1]']);
320 + assertLog([
321 + 'Pending: false',
322 + 'A1',
323 + 'Suspend! [B1]',
324 +
325 + ...(gate('enableSiblingPrerendering')
326 + ? ['Pending: false', 'A1', 'Suspend! [B1]', 'Suspend! [C1]']
327 + : []),
328 + ]);
329 expect(root).toMatchRenderedOutput(
330 <>
331 <span>Pending: true</span>
@@ -317,7 +333,16 @@ describe('ReactAsyncActions', () => {
333 </>,
334 );
335 await act(() => resolveText('B1'));
320 - assertLog(['Pending: false', 'A1', 'B1', 'Suspend! [C1]']);
336 + assertLog([
337 + 'Pending: false',
338 + 'A1',
339 + 'B1',
340 + 'Suspend! [C1]',
341 +
342 + ...(gate('enableSiblingPrerendering')
343 + ? ['Pending: false', 'A1', 'B1', 'Suspend! [C1]']
344 + : []),
345 + ]);
346 expect(root).toMatchRenderedOutput(
347 <>
348 <span>Pending: true</span>
@@ -690,6 +715,10 @@ describe('ReactAsyncActions', () => {
715 // automatically reverted.
716 'Pending: false',
717 'Suspend! [B]',
718 +
719 + ...(gate('enableSiblingPrerendering')
720 + ? ['Pending: false', 'Suspend! [B]']
721 + : []),
722 ]);
723
724 // Resolve the transition
packages/react-reconciler/src/__tests__/ReactCPUSuspense-test.js
+5 -1
@@ -231,7 +231,11 @@ describe('ReactSuspenseWithNoopRenderer', () => {
231 );
232 });
233 // Inner contents suspended, so we continue showing a fallback.
234 - assertLog(['Suspend! [Inner]']);
234 + assertLog([
235 + 'Suspend! [Inner]',
236 +
237 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [Inner]'] : []),
238 + ]);
239 expect(root).toMatchRenderedOutput(
240 <>
241 Outer
packages/react-reconciler/src/__tests__/ReactConcurrentErrorRecovery-test.js
+65 -9
@@ -209,7 +209,16 @@ describe('ReactConcurrentErrorRecovery', () => {
209 root.render(<App step={2} />);
210 });
211 });
212 - assertLog(['Suspend! [A2]', 'Loading...', 'Suspend! [B2]', 'Loading...']);
212 + assertLog([
213 + 'Suspend! [A2]',
214 + 'Loading...',
215 + 'Suspend! [B2]',
216 + 'Loading...',
217 +
218 + ...(gate('enableSiblingPrerendering')
219 + ? ['Suspend! [A2]', 'Loading...', 'Suspend! [B2]', 'Loading...']
220 + : []),
221 + ]);
222 // Because this is a refresh, we don't switch to a fallback
223 expect(root).toMatchRenderedOutput('A1B1');
224
@@ -220,7 +229,16 @@ describe('ReactConcurrentErrorRecovery', () => {
229
230 // Because we're still suspended on A, we can't show an error boundary. We
231 // should wait for A to resolve.
223 - assertLog(['Suspend! [A2]', 'Loading...', 'Error! [B2]', 'Oops!']);
232 + assertLog([
233 + 'Suspend! [A2]',
234 + 'Loading...',
235 + 'Error! [B2]',
236 + 'Oops!',
237 +
238 + ...(gate('enableSiblingPrerendering')
239 + ? ['Suspend! [A2]', 'Loading...', 'Error! [B2]', 'Oops!']
240 + : []),
241 + ]);
242 // Remain on previous screen.
243 expect(root).toMatchRenderedOutput('A1B1');
244
@@ -281,7 +299,16 @@ describe('ReactConcurrentErrorRecovery', () => {
299 root.render(<App step={2} />);
300 });
301 });
284 - assertLog(['Suspend! [A2]', 'Loading...', 'Suspend! [B2]', 'Loading...']);
302 + assertLog([
303 + 'Suspend! [A2]',
304 + 'Loading...',
305 + 'Suspend! [B2]',
306 + 'Loading...',
307 +
308 + ...(gate('enableSiblingPrerendering')
309 + ? ['Suspend! [A2]', 'Loading...', 'Suspend! [B2]', 'Loading...']
310 + : []),
311 + ]);
312 // Because this is a refresh, we don't switch to a fallback
313 expect(root).toMatchRenderedOutput('A1B1');
314
@@ -292,7 +319,16 @@ describe('ReactConcurrentErrorRecovery', () => {
319
320 // Because we're still suspended on B, we can't show an error boundary. We
321 // should wait for B to resolve.
295 - assertLog(['Error! [A2]', 'Oops!', 'Suspend! [B2]', 'Loading...']);
322 + assertLog([
323 + 'Error! [A2]',
324 + 'Oops!',
325 + 'Suspend! [B2]',
326 + 'Loading...',
327 +
328 + ...(gate('enableSiblingPrerendering')
329 + ? ['Error! [A2]', 'Oops!', 'Suspend! [B2]', 'Loading...']
330 + : []),
331 + ]);
332 // Remain on previous screen.
333 expect(root).toMatchRenderedOutput('A1B1');
334
@@ -328,7 +364,11 @@ describe('ReactConcurrentErrorRecovery', () => {
364 root.render(<AsyncText text="Async" />);
365 });
366 });
331 - assertLog(['Suspend! [Async]']);
367 + assertLog([
368 + 'Suspend! [Async]',
369 +
370 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [Async]'] : []),
371 + ]);
372 expect(root).toMatchRenderedOutput(null);
373
374 // This also works if the suspended component is wrapped with an error
@@ -344,7 +384,11 @@ describe('ReactConcurrentErrorRecovery', () => {
384 );
385 });
386 });
347 - assertLog(['Suspend! [Async]']);
387 + assertLog([
388 + 'Suspend! [Async]',
389 +
390 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [Async]'] : []),
391 + ]);
392 expect(root).toMatchRenderedOutput(null);
393
394 // Continues rendering once data resolves
@@ -397,8 +441,14 @@ describe('ReactConcurrentErrorRecovery', () => {
441 );
442 });
443 });
400 - assertLog(['Suspend! [Async]']);
401 - // The render suspended without committing or surfacing the error.
444 + assertLog([
445 + 'Suspend! [Async]',
446 +
447 + ...(gate('enableSiblingPrerendering')
448 + ? ['Suspend! [Async]', 'Caught an error: Oops!']
449 + : []),
450 + ]);
451 + // The render suspended without committing the error.
452 expect(root).toMatchRenderedOutput(null);
453
454 // Try the reverse order, too: throw then suspend
@@ -414,7 +464,13 @@ describe('ReactConcurrentErrorRecovery', () => {
464 );
465 });
466 });
417 - assertLog(['Suspend! [Async]']);
467 + assertLog([
468 + 'Suspend! [Async]',
469 +
470 + ...(gate('enableSiblingPrerendering')
471 + ? ['Suspend! [Async]', 'Caught an error: Oops!']
472 + : []),
473 + ]);
474 expect(root).toMatchRenderedOutput(null);
475
476 await act(async () => {
packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js
+10
@@ -420,6 +420,8 @@ describe('ReactDeferredValue', () => {
420 // The initial value suspended, so we attempt the final value, which
421 // also suspends.
422 'Suspend! [Final]',
423 +
424 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [Final]'] : []),
425 ]);
426 expect(root).toMatchRenderedOutput(null);
427
@@ -459,6 +461,8 @@ describe('ReactDeferredValue', () => {
461 // The initial value suspended, so we attempt the final value, which
462 // also suspends.
463 'Suspend! [Final]',
464 +
465 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [Final]'] : []),
466 ]);
467 expect(root).toMatchRenderedOutput(null);
468
@@ -531,6 +535,8 @@ describe('ReactDeferredValue', () => {
535 // The initial value suspended, so we attempt the final value, which
536 // also suspends.
537 'Suspend! [Final]',
538 +
539 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [Final]'] : []),
540 ]);
541 expect(root).toMatchRenderedOutput(null);
542
@@ -540,6 +546,8 @@ describe('ReactDeferredValue', () => {
546 'Loading...',
547 // Still waiting for the final value.
548 'Suspend! [Final]',
549 +
550 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [Final]'] : []),
551 ]);
552 expect(root).toMatchRenderedOutput('Loading...');
553
@@ -584,6 +592,8 @@ describe('ReactDeferredValue', () => {
592 // boundaries work, where we always prefer to show the innermost
593 // loading state.)
594 'Suspend! [Content]',
595 +
596 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [Content]'] : []),
597 ]);
598 // Still showing the App preview state because the inner
599 // content suspended.
packages/react-reconciler/src/__tests__/ReactExpiration-test.js
+8 -1
@@ -652,7 +652,14 @@ describe('ReactExpiration', () => {
652 React.startTransition(() => {
653 root.render(<App step={1} />);
654 });
655 - await waitForAll(['Suspend! [A1]', 'Loading...']);
655 + await waitForAll([
656 + 'Suspend! [A1]',
657 + 'Loading...',
658 +
659 + ...(gate('enableSiblingPrerendering')
660 + ? ['Suspend! [A1]', 'B', 'C', 'Loading...']
661 + : []),
662 + ]);
663
664 // Lots of time elapses before the promise resolves
665 Scheduler.unstable_advanceTime(10000);
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
+45 -6
@@ -652,14 +652,22 @@ describe('ReactHooksWithNoopRenderer', () => {
652 React.startTransition(() => {
653 root.render(<Foo signal={false} />);
654 });
655 - await waitForAll(['Suspend!']);
655 + await waitForAll([
656 + 'Suspend!',
657 +
658 + ...(gate('enableSiblingPrerendering') ? ['Suspend!'] : []),
659 + ]);
660 expect(root).toMatchRenderedOutput(<span prop={0} />);
661
662 // Rendering again should suspend again.
663 React.startTransition(() => {
664 root.render(<Foo signal={false} />);
665 });
662 - await waitForAll(['Suspend!']);
666 + await waitForAll([
667 + 'Suspend!',
668 +
669 + ...(gate('enableSiblingPrerendering') ? ['Suspend!'] : []),
670 + ]);
671 });
672
673 it('discards render phase updates if something suspends, but not other updates in the same component', async () => {
@@ -709,14 +717,22 @@ describe('ReactHooksWithNoopRenderer', () => {
717 setLabel('B');
718 });
719
712 - await waitForAll(['Suspend!']);
720 + await waitForAll([
721 + 'Suspend!',
722 +
723 + ...(gate('enableSiblingPrerendering') ? ['Suspend!'] : []),
724 + ]);
725 expect(root).toMatchRenderedOutput(<span prop="A:0" />);
726
727 // Rendering again should suspend again.
728 React.startTransition(() => {
729 root.render(<Foo signal={false} />);
730 });
719 - await waitForAll(['Suspend!']);
731 + await waitForAll([
732 + 'Suspend!',
733 +
734 + ...(gate('enableSiblingPrerendering') ? ['Suspend!'] : []),
735 + ]);
736
737 // Flip the signal back to "cancel" the update. However, the update to
738 // label should still proceed. It shouldn't have been dropped.
@@ -3495,6 +3511,13 @@ describe('ReactHooksWithNoopRenderer', () => {
3511 'Before... Pending: true',
3512 'Suspend! [After... Pending: false]',
3513 'Loading... Pending: false',
3514 +
3515 + ...(gate('enableSiblingPrerendering')
3516 + ? [
3517 + 'Suspend! [After... Pending: false]',
3518 + 'Loading... Pending: false',
3519 + ]
3520 + : []),
3521 ]);
3522 expect(ReactNoop).toMatchRenderedOutput(
3523 <span prop="Before... Pending: true" />,
@@ -3563,7 +3586,17 @@ describe('ReactHooksWithNoopRenderer', () => {
3586
3587 await act(async () => {
3588 _setText('B');
3566 - await waitForAll(['B', 'A', 'B', 'Suspend! [B]', 'Loading']);
3589 + await waitForAll([
3590 + 'B',
3591 + 'A',
3592 + 'B',
3593 + 'Suspend! [B]',
3594 + 'Loading',
3595 +
3596 + ...(gate('enableSiblingPrerendering')
3597 + ? ['B', 'Suspend! [B]', 'Loading']
3598 + : []),
3599 + ]);
3600 await waitForAll([]);
3601 expect(ReactNoop).toMatchRenderedOutput(
3602 <>
@@ -4201,7 +4234,13 @@ describe('ReactHooksWithNoopRenderer', () => {
4234 await act(async () => {
4235 await resolveText('A');
4236 });
4204 - assertLog(['Promise resolved [A]', 'A', 'Suspend! [B]']);
4237 + assertLog([
4238 + 'Promise resolved [A]',
4239 + 'A',
4240 + 'Suspend! [B]',
4241 +
4242 + ...(gate('enableSiblingPrerendering') ? ['A', 'Suspend! [B]'] : []),
4243 + ]);
4244
4245 await act(() => {
4246 root.render(null);
packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
+31 -5
@@ -198,7 +198,11 @@ describe('ReactLazy', () => {
198
199 await resolveFakeImport(Foo);
200
201 - await waitForAll(['Foo']);
201 + await waitForAll([
202 + 'Foo',
203 +
204 + ...(gate('enableSiblingPrerendering') ? ['Foo'] : []),
205 + ]);
206 expect(root).not.toMatchRenderedOutput('FooBar');
207
208 await act(() => resolveFakeImport(Bar));
@@ -235,6 +239,13 @@ describe('ReactLazy', () => {
239 assertConsoleErrorDev([
240 'Expected the result of a dynamic import() call',
241 'Expected the result of a dynamic import() call',
242 +
243 + ...(gate('enableSiblingPrerendering')
244 + ? [
245 + 'Expected the result of a dynamic import() call',
246 + 'Expected the result of a dynamic import() call',
247 + ]
248 + : []),
249 ]);
250 expect(root).not.toMatchRenderedOutput('Hi');
251 });
@@ -1072,7 +1083,7 @@ describe('ReactLazy', () => {
1083 expect(ref.current).toBe(null);
1084
1085 await act(() => resolveFakeImport(Foo));
1075 - assertLog(['Foo']);
1086 + assertLog(['Foo', ...(gate('enableSiblingPrerendering') ? ['Foo'] : [])]);
1087
1088 await act(() => resolveFakeImport(ForwardRefBar));
1089 assertLog(['Foo', 'forwardRef', 'Bar']);
@@ -1388,7 +1399,12 @@ describe('ReactLazy', () => {
1399 expect(root).not.toMatchRenderedOutput('AB');
1400
1401 await act(() => resolveFakeImport(ChildA));
1391 - assertLog(['A', 'Init B']);
1402 + assertLog([
1403 + 'A',
1404 + 'Init B',
1405 +
1406 + ...(gate('enableSiblingPrerendering') ? ['A'] : []),
1407 + ]);
1408
1409 await act(() => resolveFakeImport(ChildB));
1410 assertLog(['A', 'B', 'Did mount: A', 'Did mount: B']);
@@ -1472,10 +1488,20 @@ describe('ReactLazy', () => {
1488 React.startTransition(() => {
1489 root.update(<Parent swap={true} />);
1490 });
1475 - await waitForAll(['Init B2', 'Loading...']);
1491 + await waitForAll([
1492 + 'Init B2',
1493 + 'Loading...',
1494 +
1495 + ...(gate('enableSiblingPrerendering') ? ['Loading...'] : []),
1496 + ]);
1497 await act(() => resolveFakeImport(ChildB2));
1498 // We need to flush to trigger the second one to load.
1478 - assertLog(['Init A2', 'Loading...']);
1499 + assertLog([
1500 + 'Init A2',
1501 + 'Loading...',
1502 +
1503 + ...(gate('enableSiblingPrerendering') ? ['Loading...'] : []),
1504 + ]);
1505 await act(() => resolveFakeImport(ChildA2));
1506 assertLog(['b', 'a', 'Did update: b', 'Did update: a']);
1507 expect(root).toMatchRenderedOutput('ba');
packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
+38 -4
@@ -136,6 +136,10 @@ describe('ReactSuspense', () => {
136 // A suspends
137 'Suspend! [A]',
138 'Loading...',
139 +
140 + ...(gate('enableSiblingPrerendering')
141 + ? ['Foo', 'Bar', 'Suspend! [A]', 'B', 'Loading...']
142 + : []),
143 ]);
144 expect(container.textContent).toEqual('');
145
@@ -275,7 +279,15 @@ describe('ReactSuspense', () => {
279 expect(container.textContent).toEqual('Loading...');
280
281 await resolveText('A');
278 - await waitForAll(['A', 'Suspend! [B]', 'Loading more...']);
282 + await waitForAll([
283 + 'A',
284 + 'Suspend! [B]',
285 + 'Loading more...',
286 +
287 + ...(gate('enableSiblingPrerendering')
288 + ? ['A', 'Suspend! [B]', 'Loading more...']
289 + : []),
290 + ]);
291
292 // By this point, we have enough info to show "A" and "Loading more..."
293 // However, we've just shown the outer fallback. So we'll delay
@@ -327,7 +339,14 @@ describe('ReactSuspense', () => {
339 // B starts loading. Parent boundary is in throttle.
340 // Still shows parent loading under throttle
341 jest.advanceTimersByTime(10);
330 - await waitForAll(['Suspend! [B]', 'Loading more...']);
342 + await waitForAll([
343 + 'Suspend! [B]',
344 + 'Loading more...',
345 +
346 + ...(gate('enableSiblingPrerendering')
347 + ? ['A', 'Suspend! [B]', 'Loading more...']
348 + : []),
349 + ]);
350 expect(container.textContent).toEqual('Loading...');
351
352 // !! B could have finished before the throttle, but we show a fallback.
@@ -361,7 +380,15 @@ describe('ReactSuspense', () => {
380 expect(container.textContent).toEqual('Loading...');
381
382 await resolveText('A');
364 - await waitForAll(['A', 'Suspend! [B]', 'Loading more...']);
383 + await waitForAll([
384 + 'A',
385 + 'Suspend! [B]',
386 + 'Loading more...',
387 +
388 + ...(gate('enableSiblingPrerendering')
389 + ? ['A', 'Suspend! [B]', 'Loading more...']
390 + : []),
391 + ]);
392
393 // By this point, we have enough info to show "A" and "Loading more..."
394 // However, we've just shown the outer fallback. So we'll delay
@@ -655,7 +682,14 @@ describe('ReactSuspense', () => {
682
683 assertLog(['Suspend! [Child 1]', 'Loading...']);
684 await resolveText('Child 1');
658 - await waitForAll(['Child 1', 'Suspend! [Child 2]']);
685 + await waitForAll([
686 + 'Child 1',
687 + 'Suspend! [Child 2]',
688 +
689 + ...(gate('enableSiblingPrerendering')
690 + ? ['Child 1', 'Suspend! [Child 2]']
691 + : []),
692 + ]);
693
694 jest.advanceTimersByTime(6000);
695
packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js
+47 -2
@@ -1150,7 +1150,19 @@ describe('ReactSuspenseEffectsSemantics', () => {
1150 await act(async () => {
1151 await resolveText('InnerAsync_1');
1152 });
1153 - assertLog(['Text:Outer render', 'Suspend:OuterAsync_1']);
1153 + assertLog([
1154 + 'Text:Outer render',
1155 + 'Suspend:OuterAsync_1',
1156 +
1157 + ...(gate('enableSiblingPrerendering')
1158 + ? [
1159 + 'Text:Outer render',
1160 + 'Suspend:OuterAsync_1',
1161 + 'Text:Inner render',
1162 + 'AsyncText:InnerAsync_1 render',
1163 + ]
1164 + : []),
1165 + ]);
1166 expect(ReactNoop).toMatchRenderedOutput(
1167 <>
1168 <span prop="Outer" hidden={true} />
@@ -1194,6 +1206,17 @@ describe('ReactSuspenseEffectsSemantics', () => {
1206 'Text:Inner render',
1207 'Suspend:InnerAsync_2',
1208 'Text:InnerFallback render',
1209 +
1210 + ...(gate('enableSiblingPrerendering')
1211 + ? [
1212 + 'Text:Outer render',
1213 + 'AsyncText:OuterAsync_1 render',
1214 + 'Text:Inner render',
1215 + 'Suspend:InnerAsync_2',
1216 + 'Text:InnerFallback render',
1217 + ]
1218 + : []),
1219 +
1220 'Text:OuterFallback destroy layout',
1221 'Text:Outer create layout',
1222 'AsyncText:OuterAsync_1 create layout',
@@ -2305,6 +2328,15 @@ describe('ReactSuspenseEffectsSemantics', () => {
2328 'Text:Function render',
2329 'AsyncText:Async_1 render',
2330 'Suspend:Async_2',
2331 +
2332 + ...(gate('enableSiblingPrerendering')
2333 + ? [
2334 + 'Text:Function render',
2335 + 'AsyncText:Async_1 render',
2336 + 'Suspend:Async_2',
2337 + 'ClassText:Class render',
2338 + ]
2339 + : []),
2340 ]);
2341 expect(ReactNoop).toMatchRenderedOutput(
2342 <>
@@ -2443,7 +2475,20 @@ describe('ReactSuspenseEffectsSemantics', () => {
2475 await act(async () => {
2476 await resolveText('A');
2477 });
2446 - assertLog(['Text:Function render', 'Suspender "B" render', 'Suspend:B']);
2478 + assertLog([
2479 + 'Text:Function render',
2480 + 'Suspender "B" render',
2481 + 'Suspend:B',
2482 +
2483 + ...(gate('enableSiblingPrerendering')
2484 + ? [
2485 + 'Text:Function render',
2486 + 'Suspender "B" render',
2487 + 'Suspend:B',
2488 + 'ClassText:Class render',
2489 + ]
2490 + : []),
2491 + ]);
2492 expect(ReactNoop).toMatchRenderedOutput(
2493 <>
2494 <span prop="Function" hidden={true} />
packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js
+175 -24
@@ -383,7 +383,13 @@ describe('ReactSuspenseList', () => {
383 );
384
385 await act(() => B.resolve());
386 - assertLog(['A', 'B', 'Suspend! [C]']);
386 + assertLog([
387 + 'A',
388 + 'B',
389 + 'Suspend! [C]',
390 +
391 + ...(gate('enableSiblingPrerendering') ? ['A', 'B', 'Suspend! [C]'] : []),
392 + ]);
393
394 expect(ReactNoop).toMatchRenderedOutput(
395 <>
@@ -459,7 +465,13 @@ describe('ReactSuspenseList', () => {
465 );
466
467 await act(() => B.resolve());
462 - assertLog(['A', 'B', 'Suspend! [C]']);
468 + assertLog([
469 + 'A',
470 + 'B',
471 + 'Suspend! [C]',
472 +
473 + ...(gate('enableSiblingPrerendering') ? ['A', 'B', 'Suspend! [C]'] : []),
474 + ]);
475
476 expect(ReactNoop).toMatchRenderedOutput(
477 <>
@@ -767,7 +779,12 @@ describe('ReactSuspenseList', () => {
779 );
780
781 await act(() => B.resolve());
770 - assertLog(['B', 'Suspend! [C]']);
782 + assertLog([
783 + 'B',
784 + 'Suspend! [C]',
785 +
786 + ...(gate('enableSiblingPrerendering') ? ['B', 'Suspend! [C]'] : []),
787 + ]);
788
789 // Even though we could now show B, we're still waiting on C.
790 expect(ReactNoop).toMatchRenderedOutput(
@@ -854,7 +871,12 @@ describe('ReactSuspenseList', () => {
871 expect(ReactNoop).toMatchRenderedOutput(<span>A</span>);
872
873 await act(() => B.resolve());
857 - assertLog(['B', 'Suspend! [C]']);
874 + assertLog([
875 + 'B',
876 + 'Suspend! [C]',
877 +
878 + ...(gate('enableSiblingPrerendering') ? ['B', 'Suspend! [C]'] : []),
879 + ]);
880
881 // Even though we could now show B, we're still waiting on C.
882 expect(ReactNoop).toMatchRenderedOutput(<span>A</span>);
@@ -908,7 +930,12 @@ describe('ReactSuspenseList', () => {
930 );
931
932 await act(() => A.resolve());
911 - assertLog(['A', 'Suspend! [B]']);
933 + assertLog([
934 + 'A',
935 + 'Suspend! [B]',
936 +
937 + ...(gate('enableSiblingPrerendering') ? ['A', 'Suspend! [B]'] : []),
938 + ]);
939
940 expect(ReactNoop).toMatchRenderedOutput(
941 <>
@@ -967,7 +994,12 @@ describe('ReactSuspenseList', () => {
994 );
995
996 await act(() => C.resolve());
970 - assertLog(['C', 'Suspend! [B]']);
997 + assertLog([
998 + 'C',
999 + 'Suspend! [B]',
1000 +
1001 + ...(gate('enableSiblingPrerendering') ? ['C', 'Suspend! [B]'] : []),
1002 + ]);
1003
1004 expect(ReactNoop).toMatchRenderedOutput(
1005 <>
@@ -1072,7 +1104,12 @@ describe('ReactSuspenseList', () => {
1104 );
1105
1106 await act(() => A.resolve());
1075 - assertLog(['A', 'Suspend! [C]']);
1107 + assertLog([
1108 + 'A',
1109 + 'Suspend! [C]',
1110 +
1111 + ...(gate('enableSiblingPrerendering') ? ['A', 'Suspend! [C]'] : []),
1112 + ]);
1113
1114 // Even though we could show A, it is still in a fallback state because
1115 // C is not yet resolved. We need to resolve everything in the head first.
@@ -1088,7 +1125,13 @@ describe('ReactSuspenseList', () => {
1125 );
1126
1127 await act(() => C.resolve());
1091 - assertLog(['A', 'C', 'Suspend! [E]']);
1128 + assertLog([
1129 + 'A',
1130 + 'C',
1131 + 'Suspend! [E]',
1132 +
1133 + ...(gate('enableSiblingPrerendering') ? ['A', 'C', 'Suspend! [E]'] : []),
1134 + ]);
1135
1136 // We can now resolve the full head.
1137 expect(ReactNoop).toMatchRenderedOutput(
@@ -1103,7 +1146,12 @@ describe('ReactSuspenseList', () => {
1146 );
1147
1148 await act(() => E.resolve());
1106 - assertLog(['E', 'Suspend! [F]']);
1149 + assertLog([
1150 + 'E',
1151 + 'Suspend! [F]',
1152 +
1153 + ...(gate('enableSiblingPrerendering') ? ['E', 'Suspend! [F]'] : []),
1154 + ]);
1155
1156 // In the tail we can resolve one-by-one.
1157 expect(ReactNoop).toMatchRenderedOutput(
@@ -1267,7 +1315,12 @@ describe('ReactSuspenseList', () => {
1315
1316 await F.resolve();
1317
1270 - await waitForAll(['Suspend! [D]', 'F']);
1318 + await waitForAll([
1319 + 'Suspend! [D]',
1320 + 'F',
1321 +
1322 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [D]', 'F'] : []),
1323 + ]);
1324
1325 // Even though we could show F, it is still in a fallback state because
1326 // E is not yet resolved. We need to resolve everything in the head first.
@@ -1287,7 +1340,13 @@ describe('ReactSuspenseList', () => {
1340 );
1341
1342 await act(() => D.resolve());
1290 - assertLog(['D', 'F', 'Suspend! [B]']);
1343 + assertLog([
1344 + 'D',
1345 + 'F',
1346 + 'Suspend! [B]',
1347 +
1348 + ...(gate('enableSiblingPrerendering') ? ['D', 'F', 'Suspend! [B]'] : []),
1349 + ]);
1350
1351 // We can now resolve the full head.
1352 expect(ReactNoop).toMatchRenderedOutput(
@@ -1304,7 +1363,12 @@ describe('ReactSuspenseList', () => {
1363 );
1364
1365 await act(() => B.resolve());
1307 - assertLog(['B', 'Suspend! [A]']);
1366 + assertLog([
1367 + 'B',
1368 + 'Suspend! [A]',
1369 +
1370 + ...(gate('enableSiblingPrerendering') ? ['B', 'Suspend! [A]'] : []),
1371 + ]);
1372
1373 // In the tail we can resolve one-by-one.
1374 expect(ReactNoop).toMatchRenderedOutput(
@@ -1429,7 +1493,15 @@ describe('ReactSuspenseList', () => {
1493
1494 await A.resolve();
1495
1432 - await waitForAll(['A', 'Suspend! [B]', 'Loading B']);
1496 + await waitForAll([
1497 + 'A',
1498 + 'Suspend! [B]',
1499 + 'Loading B',
1500 +
1501 + ...(gate('enableSiblingPrerendering')
1502 + ? ['A', 'Suspend! [B]', 'Loading B']
1503 + : []),
1504 + ]);
1505
1506 // Incremental loading is suspended.
1507 jest.advanceTimersByTime(500);
@@ -1443,7 +1515,15 @@ describe('ReactSuspenseList', () => {
1515
1516 await B.resolve();
1517
1446 - await waitForAll(['B', 'Suspend! [C]', 'Loading C']);
1518 + await waitForAll([
1519 + 'B',
1520 + 'Suspend! [C]',
1521 + 'Loading C',
1522 +
1523 + ...(gate('enableSiblingPrerendering')
1524 + ? ['B', 'Suspend! [C]', 'Loading C']
1525 + : []),
1526 + ]);
1527
1528 // Incremental loading is suspended.
1529 jest.advanceTimersByTime(500);
@@ -1667,7 +1747,12 @@ describe('ReactSuspenseList', () => {
1747
1748 await B.resolve();
1749
1670 - await waitForAll(['B', 'Suspend! [C]']);
1750 + await waitForAll([
1751 + 'B',
1752 + 'Suspend! [C]',
1753 +
1754 + ...(gate('enableSiblingPrerendering') ? ['B', 'Suspend! [C]'] : []),
1755 + ]);
1756
1757 // Incremental loading is suspended.
1758 jest.advanceTimersByTime(500);
@@ -1687,7 +1772,17 @@ describe('ReactSuspenseList', () => {
1772 await C.resolve();
1773 await E.resolve();
1774
1690 - await waitForAll(['B', 'C', 'E', 'Suspend! [F]', 'Loading F']);
1775 + await waitForAll([
1776 + 'B',
1777 + 'C',
1778 + 'E',
1779 + 'Suspend! [F]',
1780 + 'Loading F',
1781 +
1782 + ...(gate('enableSiblingPrerendering')
1783 + ? ['B', 'C', 'E', 'Suspend! [F]', 'Loading F']
1784 + : []),
1785 + ]);
1786
1787 jest.advanceTimersByTime(500);
1788
@@ -1804,7 +1899,12 @@ describe('ReactSuspenseList', () => {
1899
1900 await D.resolve();
1901
1807 - await waitForAll(['D', 'Suspend! [E]']);
1902 + await waitForAll([
1903 + 'D',
1904 + 'Suspend! [E]',
1905 +
1906 + ...(gate('enableSiblingPrerendering') ? ['D', 'Suspend! [E]'] : []),
1907 + ]);
1908
1909 // Incremental loading is suspended.
1910 jest.advanceTimersByTime(500);
@@ -1829,7 +1929,17 @@ describe('ReactSuspenseList', () => {
1929 await D.resolve();
1930 await E.resolve();
1931
1832 - await waitForAll(['D', 'E', 'B', 'Suspend! [A]', 'Loading A']);
1932 + await waitForAll([
1933 + 'D',
1934 + 'E',
1935 + 'B',
1936 + 'Suspend! [A]',
1937 + 'Loading A',
1938 +
1939 + ...(gate('enableSiblingPrerendering')
1940 + ? ['D', 'E', 'B', 'Suspend! [A]', 'Loading A']
1941 + : []),
1942 + ]);
1943
1944 jest.advanceTimersByTime(500);
1945
@@ -1958,7 +2068,12 @@ describe('ReactSuspenseList', () => {
2068
2069 await B.resolve();
2070
1961 - await waitForAll(['B', 'Suspend! [C]']);
2071 + await waitForAll([
2072 + 'B',
2073 + 'Suspend! [C]',
2074 +
2075 + ...(gate('enableSiblingPrerendering') ? ['B', 'Suspend! [C]'] : []),
2076 + ]);
2077
2078 // Incremental loading is suspended.
2079 jest.advanceTimersByTime(500);
@@ -1981,7 +2096,17 @@ describe('ReactSuspenseList', () => {
2096 await D.resolve();
2097 await E.resolve();
2098
1984 - await waitForAll(['C', 'D', 'E', 'Suspend! [F]', 'Loading F']);
2099 + await waitForAll([
2100 + 'C',
2101 + 'D',
2102 + 'E',
2103 + 'Suspend! [F]',
2104 + 'Loading F',
2105 +
2106 + ...(gate('enableSiblingPrerendering')
2107 + ? ['C', 'D', 'E', 'Suspend! [F]', 'Loading F']
2108 + : []),
2109 + ]);
2110
2111 jest.advanceTimersByTime(500);
2112
@@ -2044,7 +2169,15 @@ describe('ReactSuspenseList', () => {
2169
2170 await A.resolve();
2171
2047 - await waitForAll(['A', 'Suspend! [B]', 'Loading B']);
2172 + await waitForAll([
2173 + 'A',
2174 + 'Suspend! [B]',
2175 + 'Loading B',
2176 +
2177 + ...(gate('enableSiblingPrerendering')
2178 + ? ['A', 'Suspend! [B]', 'Loading B']
2179 + : []),
2180 + ]);
2181
2182 // Incremental loading is suspended.
2183 jest.advanceTimersByTime(500);
@@ -2052,7 +2185,15 @@ describe('ReactSuspenseList', () => {
2185 expect(ReactNoop).toMatchRenderedOutput(<span>A</span>);
2186
2187 await act(() => B.resolve());
2055 - assertLog(['B', 'Suspend! [C]', 'Loading C']);
2188 + assertLog([
2189 + 'B',
2190 + 'Suspend! [C]',
2191 + 'Loading C',
2192 +
2193 + ...(gate('enableSiblingPrerendering')
2194 + ? ['B', 'Suspend! [C]', 'Loading C']
2195 + : []),
2196 + ]);
2197
2198 // Incremental loading is suspended.
2199 jest.advanceTimersByTime(500);
@@ -2787,7 +2928,12 @@ describe('ReactSuspenseList', () => {
2928 expect(onRender.mock.calls[2][3]).toBe(1 + 4 + 3 + 3);
2929
2930 await act(() => C.resolve());
2790 - assertLog(['C', 'Suspend! [D]']);
2931 + assertLog([
2932 + 'C',
2933 + 'Suspend! [D]',
2934 +
2935 + ...(gate('enableSiblingPrerendering') ? ['C', 'Suspend! [D]'] : []),
2936 + ]);
2937 expect(ReactNoop).toMatchRenderedOutput(
2938 <>
2939 <span>A</span>
@@ -2873,7 +3019,12 @@ describe('ReactSuspenseList', () => {
3019 );
3020
3021 await act(() => A.resolve());
2876 - assertLog(['A', 'Suspend! [B]']);
3022 + assertLog([
3023 + 'A',
3024 + 'Suspend! [B]',
3025 +
3026 + ...(gate('enableSiblingPrerendering') ? ['A', 'Suspend! [B]'] : []),
3027 + ]);
3028 expect(ReactNoop).toMatchRenderedOutput(
3029 <>
3030 <span>A</span>
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
+232 -24
@@ -298,6 +298,19 @@ describe('ReactSuspenseWithNoopRenderer', () => {
298 // We immediately unwind and switch to a fallback without
299 // rendering siblings.
300 'Loading...',
301 +
302 + ...(gate('enableSiblingPrerendering')
303 + ? [
304 + 'Foo',
305 + 'Bar',
306 + // A suspends
307 + 'Suspend! [A]',
308 + 'B',
309 + // We immediately unwind and switch to a fallback without
310 + // rendering siblings.
311 + 'Loading...',
312 + ]
313 + : []),
314 ]);
315 expect(ReactNoop).toMatchRenderedOutput(null);
316
@@ -379,7 +392,15 @@ describe('ReactSuspenseWithNoopRenderer', () => {
392 });
393
394 // B suspends. Render a fallback
382 - await waitForAll(['A', 'Suspend! [B]', 'Loading...']);
395 + await waitForAll([
396 + 'A',
397 + 'Suspend! [B]',
398 + 'Loading...',
399 +
400 + ...(gate('enableSiblingPrerendering')
401 + ? ['A', 'Suspend! [B]', 'C', 'D', 'Loading...']
402 + : []),
403 + ]);
404 // Did not commit yet.
405 expect(ReactNoop).toMatchRenderedOutput(null);
406
@@ -436,7 +457,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
457 React.startTransition(() => {
458 ReactNoop.render(<App renderContent={true} />);
459 });
439 - await waitForAll(['Suspend! [Result]', 'Loading...']);
460 + await waitForAll([
461 + 'Suspend! [Result]',
462 + 'Loading...',
463 +
464 + ...(gate('enableSiblingPrerendering')
465 + ? ['Suspend! [Result]', 'Loading...']
466 + : []),
467 + ]);
468 expect(ReactNoop).toMatchRenderedOutput(null);
469
470 await rejectText('Result', new Error('Failed to load: Result'));
@@ -496,6 +524,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
524 // React retries one more time
525 'Error! [Result]',
526
527 + ...(gate('enableSiblingPrerendering')
528 + ? ['Error! [Result]', 'Error! [Result]']
529 + : []),
530 +
531 // Errored again on retry. Now handle it.
532 'Caught error: Failed to load: Result',
533 ]);
@@ -547,7 +579,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
579 // Update the low-pri text
580 await act(() => startTransition(() => setLowPri('2')));
581 // Suspends
550 - assertLog(['Suspend! [2]', 'Loading...']);
582 + assertLog([
583 + 'Suspend! [2]',
584 + 'Loading...',
585 +
586 + ...(gate('enableSiblingPrerendering')
587 + ? ['Suspend! [2]', 'Loading...']
588 + : []),
589 + ]);
590
591 // While we're still waiting for the low-pri update to complete, update the
592 // high-pri text at high priority.
@@ -592,13 +631,27 @@ describe('ReactSuspenseWithNoopRenderer', () => {
631 React.startTransition(() => {
632 ReactNoop.render(<App showA={true} showB={false} />);
633 });
595 - await waitForAll(['Suspend! [A]', 'Loading...']);
634 + await waitForAll([
635 + 'Suspend! [A]',
636 + 'Loading...',
637 +
638 + ...(gate('enableSiblingPrerendering')
639 + ? ['Suspend! [A]', 'Loading...']
640 + : []),
641 + ]);
642 expect(ReactNoop).toMatchRenderedOutput(null);
643
644 React.startTransition(() => {
645 ReactNoop.render(<App showA={true} showB={true} />);
646 });
601 - await waitForAll(['Suspend! [A]', 'Loading...']);
647 + await waitForAll([
648 + 'Suspend! [A]',
649 + 'Loading...',
650 +
651 + ...(gate('enableSiblingPrerendering')
652 + ? ['Suspend! [A]', 'B', 'Loading...']
653 + : []),
654 + ]);
655 expect(ReactNoop).toMatchRenderedOutput(null);
656
657 await resolveText('A');
@@ -743,6 +796,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
796 'Outer content',
797 'Suspend! [Inner content]',
798 'Loading inner...',
799 +
800 + ...(gate('enableSiblingPrerendering')
801 + ? ['Outer content', 'Suspend! [Inner content]', 'Loading inner...']
802 + : []),
803 ]);
804 // Don't commit the inner placeholder yet.
805 expect(ReactNoop).toMatchRenderedOutput(
@@ -932,7 +989,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
989 </Suspense>,
990 );
991 });
935 - await waitForAll(['Suspend! [Async]', 'Loading...']);
992 + await waitForAll([
993 + 'Suspend! [Async]',
994 + 'Loading...',
995 +
996 + ...(gate('enableSiblingPrerendering')
997 + ? ['Suspend! [Async]', 'Loading...']
998 + : []),
999 + ]);
1000 expect(ReactNoop).toMatchRenderedOutput(null);
1001
1002 // Resolve the promise
@@ -964,7 +1028,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1028 // Schedule an update, and suspend for up to 5 seconds.
1029 React.startTransition(() => ReactNoop.render(<App text="A" />));
1030 // The update should suspend.
967 - await waitForAll(['Suspend! [A]', 'Loading...']);
1031 + await waitForAll([
1032 + 'Suspend! [A]',
1033 + 'Loading...',
1034 +
1035 + ...(gate('enableSiblingPrerendering')
1036 + ? ['Suspend! [A]', 'Loading...']
1037 + : []),
1038 + ]);
1039 expect(ReactNoop).toMatchRenderedOutput(<span prop="S" />);
1040
1041 // Advance time until right before it expires.
@@ -976,7 +1047,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1047 // Schedule another low priority update.
1048 React.startTransition(() => ReactNoop.render(<App text="B" />));
1049 // This update should also suspend.
979 - await waitForAll(['Suspend! [B]', 'Loading...']);
1050 + await waitForAll([
1051 + 'Suspend! [B]',
1052 + 'Loading...',
1053 +
1054 + ...(gate('enableSiblingPrerendering')
1055 + ? ['Suspend! [B]', 'Loading...']
1056 + : []),
1057 + ]);
1058 expect(ReactNoop).toMatchRenderedOutput(<span prop="S" />);
1059
1060 // Schedule a regular update. Its expiration time will fall between
@@ -1747,6 +1825,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1825 // B suspends
1826 'Suspend! [B]',
1827 'Loading more...',
1828 +
1829 + ...(gate('enableSiblingPrerendering')
1830 + ? ['A', 'Suspend! [B]', 'Loading more...']
1831 + : []),
1832 ]);
1833 // Because we've already been waiting for so long we can
1834 // wait a bit longer. Still nothing...
@@ -2238,7 +2320,16 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2320 ReactNoop.render(<Foo showB={true} />);
2321 });
2322
2241 - await waitForAll(['Foo', 'A', 'Suspend! [B]', 'Loading B...']);
2323 + await waitForAll([
2324 + 'Foo',
2325 + 'A',
2326 + 'Suspend! [B]',
2327 + 'Loading B...',
2328 +
2329 + ...(gate('enableSiblingPrerendering')
2330 + ? ['Foo', 'A', 'Suspend! [B]', 'Loading B...']
2331 + : []),
2332 + ]);
2333
2334 // Transitions never fall back.
2335 expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
@@ -2314,7 +2405,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2405 // Start transition.
2406 React.startTransition(() => ReactNoop.render(<App page="B" />));
2407
2317 - await waitForAll(['Suspend! [B]', 'Loading...']);
2408 + await waitForAll([
2409 + 'Suspend! [B]',
2410 + 'Loading...',
2411 +
2412 + ...(gate('enableSiblingPrerendering')
2413 + ? ['Suspend! [B]', 'Loading...']
2414 + : []),
2415 + ]);
2416 Scheduler.unstable_advanceTime(100000);
2417 await advanceTimers(100000);
2418 // Even after lots of time has passed, we have still not yet flushed the
@@ -2365,7 +2463,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2463 await act(async () => {
2464 React.startTransition(() => transitionToPage('B'));
2465
2368 - await waitForAll(['Suspend! [B]', 'Loading...']);
2466 + await waitForAll([
2467 + 'Suspend! [B]',
2468 + 'Loading...',
2469 +
2470 + ...(gate('enableSiblingPrerendering')
2471 + ? ['Suspend! [B]', 'Loading...']
2472 + : []),
2473 + ]);
2474 Scheduler.unstable_advanceTime(100000);
2475 await advanceTimers(100000);
2476 // Even after lots of time has passed, we have still not yet flushed the
@@ -2420,7 +2525,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2525 await act(async () => {
2526 React.startTransition(() => transitionToPage('B'));
2527
2423 - await waitForAll(['Suspend! [B]', 'Loading...']);
2528 + await waitForAll([
2529 + 'Suspend! [B]',
2530 + 'Loading...',
2531 +
2532 + ...(gate('enableSiblingPrerendering')
2533 + ? ['Suspend! [B]', 'Loading...']
2534 + : []),
2535 + ]);
2536 Scheduler.unstable_advanceTime(100000);
2537 await advanceTimers(100000);
2538 // Even after lots of time has passed, we have still not yet flushed the
@@ -2462,7 +2574,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2574 // Start transition.
2575 React.startTransition(() => ReactNoop.render(<App page="B" />));
2576
2465 - await waitForAll(['Suspend! [B]', 'Loading...']);
2577 + await waitForAll([
2578 + 'Suspend! [B]',
2579 + 'Loading...',
2580 +
2581 + ...(gate('enableSiblingPrerendering')
2582 + ? ['Suspend! [B]', 'Loading...']
2583 + : []),
2584 + ]);
2585 Scheduler.unstable_advanceTime(2999);
2586 await advanceTimers(2999);
2587 // Since the timeout is infinite (or effectively infinite),
@@ -2476,7 +2595,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2595
2596 // Start a long (infinite) transition.
2597 React.startTransition(() => ReactNoop.render(<App page="C" />));
2479 - await waitForAll(['Suspend! [C]', 'Loading...']);
2598 + await waitForAll([
2599 + 'Suspend! [C]',
2600 + 'Loading...',
2601 +
2602 + ...(gate('enableSiblingPrerendering')
2603 + ? ['Suspend! [C]', 'Loading...']
2604 + : []),
2605 + ]);
2606
2607 // Even after lots of time has passed, we have still not yet flushed the
2608 // loading state.
@@ -2524,7 +2650,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2650 await act(async () => {
2651 React.startTransition(() => transitionToPage('B'));
2652
2527 - await waitForAll(['Suspend! [B]', 'Loading...']);
2653 + await waitForAll([
2654 + 'Suspend! [B]',
2655 + 'Loading...',
2656 +
2657 + ...(gate('enableSiblingPrerendering')
2658 + ? ['Suspend! [B]', 'Loading...']
2659 + : []),
2660 + ]);
2661
2662 Scheduler.unstable_advanceTime(2999);
2663 await advanceTimers(2999);
@@ -2542,7 +2675,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2675 await act(async () => {
2676 React.startTransition(() => transitionToPage('C'));
2677
2545 - await waitForAll(['Suspend! [C]', 'Loading...']);
2678 + await waitForAll([
2679 + 'Suspend! [C]',
2680 + 'Loading...',
2681 +
2682 + ...(gate('enableSiblingPrerendering')
2683 + ? ['Suspend! [C]', 'Loading...']
2684 + : []),
2685 + ]);
2686
2687 // Even after lots of time has passed, we have still not yet flushed the
2688 // loading state.
@@ -2594,7 +2734,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2734 await act(async () => {
2735 React.startTransition(() => transitionToPage('B'));
2736
2597 - await waitForAll(['Suspend! [B]', 'Loading...']);
2737 + await waitForAll([
2738 + 'Suspend! [B]',
2739 + 'Loading...',
2740 +
2741 + ...(gate('enableSiblingPrerendering')
2742 + ? ['Suspend! [B]', 'Loading...']
2743 + : []),
2744 + ]);
2745 Scheduler.unstable_advanceTime(2999);
2746 await advanceTimers(2999);
2747 // Since the timeout is infinite (or effectively infinite),
@@ -2611,7 +2758,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2758 await act(async () => {
2759 React.startTransition(() => transitionToPage('C'));
2760
2614 - await waitForAll(['Suspend! [C]', 'Loading...']);
2761 + await waitForAll([
2762 + 'Suspend! [C]',
2763 + 'Loading...',
2764 +
2765 + ...(gate('enableSiblingPrerendering')
2766 + ? ['Suspend! [C]', 'Loading...']
2767 + : []),
2768 + ]);
2769
2770 // Even after lots of time has passed, we have still not yet flushed the
2771 // loading state.
@@ -2652,7 +2806,15 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2806 // Start transition.
2807 React.startTransition(() => ReactNoop.render(<App page="B" />));
2808
2655 - await waitForAll(['Hi!', 'Suspend! [B]', 'Loading B...']);
2809 + await waitForAll([
2810 + 'Hi!',
2811 + 'Suspend! [B]',
2812 + 'Loading B...',
2813 +
2814 + ...(gate('enableSiblingPrerendering')
2815 + ? ['Hi!', 'Suspend! [B]', 'Loading B...']
2816 + : []),
2817 + ]);
2818
2819 // Suspended
2820 expect(ReactNoop).toMatchRenderedOutput(
@@ -2713,7 +2875,15 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2875 // Start transition.
2876 React.startTransition(() => ReactNoop.render(<App page="B" />));
2877
2716 - await waitForAll(['Hi!', 'Suspend! [B]', 'Loading B...']);
2878 + await waitForAll([
2879 + 'Hi!',
2880 + 'Suspend! [B]',
2881 + 'Loading B...',
2882 +
2883 + ...(gate('enableSiblingPrerendering')
2884 + ? ['Hi!', 'Suspend! [B]', 'Loading B...']
2885 + : []),
2886 + ]);
2887
2888 // Suspended
2889 expect(ReactNoop).toMatchRenderedOutput(
@@ -2827,7 +2997,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2997 ReactNoop.render(<App showContent={true} />);
2998 });
2999
2830 - await waitForAll(['Suspend! [A]', 'Loading...']);
3000 + await waitForAll([
3001 + 'Suspend! [A]',
3002 + 'Loading...',
3003 +
3004 + ...(gate('enableSiblingPrerendering')
3005 + ? ['Suspend! [A]', 'Loading...']
3006 + : []),
3007 + ]);
3008 await resolveText('A');
3009 await waitFor(['A', 'Commit']);
3010 expect(ReactNoop).toMatchRenderedOutput(
@@ -2879,7 +3056,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3056 ReactNoop.render(<App showContent={true} />);
3057 });
3058
2882 - await waitForAll(['Suspend! [A]', 'Loading...']);
3059 + await waitForAll([
3060 + 'Suspend! [A]',
3061 + 'Loading...',
3062 +
3063 + ...(gate('enableSiblingPrerendering')
3064 + ? ['Suspend! [A]', 'Loading...']
3065 + : []),
3066 + ]);
3067 await resolveText('A');
3068 await waitFor(['A', 'Commit']);
3069 expect(ReactNoop).toMatchRenderedOutput(
@@ -3492,6 +3676,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3676 );
3677 });
3678
3679 + // This regression test relies on subtle implementation details that happen to
3680 + // rely on sibling prerendering being disabled. Not going to bother to rewrite
3681 + // it for now; maybe once we land the experiment.
3682 + // @gate !enableSiblingPrerendering
3683 // @gate enableLegacyCache
3684 it('regression: ping at high priority causes update to be dropped', async () => {
3685 const {useState, useTransition} = React;
@@ -3654,7 +3842,16 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3842 // Update to "a". That will suspend.
3843 await act(async () => {
3844 setTextWithShortTransition('a');
3657 - await waitForAll(['Pending...', '', 'Suspend! [a]', 'Loading...']);
3845 + await waitForAll([
3846 + 'Pending...',
3847 + '',
3848 + 'Suspend! [a]',
3849 + 'Loading...',
3850 +
3851 + ...(gate('enableSiblingPrerendering')
3852 + ? ['Suspend! [a]', 'Loading...']
3853 + : []),
3854 + ]);
3855 });
3856 assertLog([]);
3857 expect(root).toMatchRenderedOutput(
@@ -3673,6 +3870,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3870 '',
3871 'Suspend! [b]',
3872 'Loading...',
3873 +
3874 + ...(gate('enableSiblingPrerendering')
3875 + ? ['Suspend! [b]', 'Loading...']
3876 + : []),
3877 ]);
3878 });
3879 assertLog([]);
@@ -3687,7 +3888,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3888 await act(async () => {
3889 await resolveText('a');
3890
3690 - await waitForAll(['Suspend! [b]', 'Loading...']);
3891 + await waitForAll([
3892 + 'Suspend! [b]',
3893 + 'Loading...',
3894 +
3895 + ...(gate('enableSiblingPrerendering')
3896 + ? ['Suspend! [b]', 'Loading...']
3897 + : []),
3898 + ]);
3899 expect(root).toMatchRenderedOutput(
3900 <>
3901 <span prop="Pending..." />
packages/react-reconciler/src/__tests__/ReactTransition-test.js
+72 -5
@@ -199,6 +199,10 @@ describe('ReactTransition', () => {
199 '(empty)',
200 'Suspend! [Async]',
201 'Loading...',
202 +
203 + ...(gate('enableSiblingPrerendering')
204 + ? ['Suspend! [Async]', 'Loading...']
205 + : []),
206 ]);
207
208 expect(root).toMatchRenderedOutput('Pending...(empty)');
@@ -269,6 +273,10 @@ describe('ReactTransition', () => {
273 'B label',
274 'Suspend! [B content]',
275 'Loading...',
276 +
277 + ...(gate('enableSiblingPrerendering')
278 + ? ['B label', 'Suspend! [B content]', 'Loading...']
279 + : []),
280 ]);
281 // This is a refresh transition so it shouldn't show a fallback
282 expect(root).toMatchRenderedOutput(
@@ -290,6 +298,10 @@ describe('ReactTransition', () => {
298 'C label',
299 'Suspend! [C content]',
300 'Loading...',
301 +
302 + ...(gate('enableSiblingPrerendering')
303 + ? ['C label', 'Suspend! [C content]', 'Loading...']
304 + : []),
305 ]);
306 expect(root).toMatchRenderedOutput(
307 <>
@@ -307,6 +319,10 @@ describe('ReactTransition', () => {
319 'C label',
320 'Suspend! [C content]',
321 'Loading...',
322 +
323 + ...(gate('enableSiblingPrerendering')
324 + ? ['C label', 'Suspend! [C content]', 'Loading...']
325 + : []),
326 ]);
327 expect(root).toMatchRenderedOutput(
328 <>
@@ -394,6 +410,10 @@ describe('ReactTransition', () => {
410 'B label',
411 'Suspend! [B content]',
412 'Loading...',
413 +
414 + ...(gate('enableSiblingPrerendering')
415 + ? ['B label', 'Suspend! [B content]', 'Loading...']
416 + : []),
417 ]);
418 // This is a refresh transition so it shouldn't show a fallback
419 expect(root).toMatchRenderedOutput(
@@ -415,6 +435,10 @@ describe('ReactTransition', () => {
435 'C label',
436 'Suspend! [C content]',
437 'Loading...',
438 +
439 + ...(gate('enableSiblingPrerendering')
440 + ? ['C label', 'Suspend! [C content]', 'Loading...']
441 + : []),
442 ]);
443 expect(root).toMatchRenderedOutput(
444 <>
@@ -432,6 +456,10 @@ describe('ReactTransition', () => {
456 'C label',
457 'Suspend! [C content]',
458 'Loading...',
459 +
460 + ...(gate('enableSiblingPrerendering')
461 + ? ['C label', 'Suspend! [C content]', 'Loading...']
462 + : []),
463 ]);
464 expect(root).toMatchRenderedOutput(
465 <>
@@ -500,7 +528,14 @@ describe('ReactTransition', () => {
528 setShowA(true);
529 });
530 });
503 - assertLog(['Suspend! [A]', 'Loading...']);
531 + assertLog([
532 + 'Suspend! [A]',
533 + 'Loading...',
534 +
535 + ...(gate('enableSiblingPrerendering')
536 + ? ['Suspend! [A]', 'Loading...']
537 + : []),
538 + ]);
539 expect(root).toMatchRenderedOutput(null);
540
541 // Before A loads, switch to B. This should entangle A with B.
@@ -510,7 +545,14 @@ describe('ReactTransition', () => {
545 setShowB(true);
546 });
547 });
513 - assertLog(['Suspend! [B]', 'Loading...']);
548 + assertLog([
549 + 'Suspend! [B]',
550 + 'Loading...',
551 +
552 + ...(gate('enableSiblingPrerendering')
553 + ? ['Suspend! [B]', 'Loading...']
554 + : []),
555 + ]);
556 expect(root).toMatchRenderedOutput(null);
557
558 // Before A or B loads, switch to C. This should entangle C with B, and
@@ -521,7 +563,14 @@ describe('ReactTransition', () => {
563 setShowC(true);
564 });
565 });
524 - assertLog(['Suspend! [C]', 'Loading...']);
566 + assertLog([
567 + 'Suspend! [C]',
568 + 'Loading...',
569 +
570 + ...(gate('enableSiblingPrerendering')
571 + ? ['Suspend! [C]', 'Loading...']
572 + : []),
573 + ]);
574 expect(root).toMatchRenderedOutput(null);
575
576 // Now the data starts resolving out of order.
@@ -533,7 +582,14 @@ describe('ReactTransition', () => {
582 resolveText('B');
583 });
584 });
536 - assertLog(['Suspend! [C]', 'Loading...']);
585 + assertLog([
586 + 'Suspend! [C]',
587 + 'Loading...',
588 +
589 + ...(gate('enableSiblingPrerendering')
590 + ? ['Suspend! [C]', 'Loading...']
591 + : []),
592 + ]);
593 expect(root).toMatchRenderedOutput(null);
594
595 // Now resolve A. Again, this will attempt to render C, since everything
@@ -543,7 +599,14 @@ describe('ReactTransition', () => {
599 resolveText('A');
600 });
601 });
546 - assertLog(['Suspend! [C]', 'Loading...']);
602 + assertLog([
603 + 'Suspend! [C]',
604 + 'Loading...',
605 +
606 + ...(gate('enableSiblingPrerendering')
607 + ? ['Suspend! [C]', 'Loading...']
608 + : []),
609 + ]);
610 expect(root).toMatchRenderedOutput(null);
611
612 // Finally, resolve C. This time we can finish.
@@ -861,6 +924,10 @@ describe('ReactTransition', () => {
924 // Suspend.
925 'Suspend! [Async]',
926 'Loading...',
927 +
928 + ...(gate('enableSiblingPrerendering')
929 + ? ['Suspend! [Async]', 'Normal pri: 0', 'Loading...']
930 + : []),
931 ]);
932 expect(root).toMatchRenderedOutput('(empty), Normal pri: 0');
933
packages/react-reconciler/src/__tests__/ReactUse-test.js
+40 -8
@@ -1066,13 +1066,25 @@ describe('ReactUse', () => {
1066 await act(() => {
1067 resolveTextRequests('A');
1068 });
1069 - assertLog(['A', '(Loading B...)']);
1069 + assertLog([
1070 + 'A',
1071 + '(Loading B...)',
1072 +
1073 + ...(gate('enableSiblingPrerendering')
1074 + ? ['A', '(Loading C...)', '(Loading B...)']
1075 + : []),
1076 + ]);
1077 expect(root).toMatchRenderedOutput('A(Loading B...)');
1078
1079 await act(() => {
1080 resolveTextRequests('B');
1081 });
1075 - assertLog(['B', '(Loading C...)']);
1082 + assertLog([
1083 + 'B',
1084 + '(Loading C...)',
1085 +
1086 + ...(gate('enableSiblingPrerendering') ? ['B', '(Loading C...)'] : []),
1087 + ]);
1088 expect(root).toMatchRenderedOutput('AB(Loading C...)');
1089
1090 await act(() => {
@@ -1868,16 +1880,26 @@ describe('ReactUse', () => {
1880
1881 await expect(async () => {
1882 await act(() => resolveTextRequests('Hi'));
1871 - }).toErrorDev(
1883 + }).toErrorDev([
1884 // We get this warning because the generator's promise themselves are not cached.
1885 'A component was suspended by an uncached promise. Creating ' +
1886 'promises inside a Client Component or hook is not yet ' +
1887 'supported, except via a Suspense-compatible library or framework.',
1876 - );
1888 +
1889 + ...(gate('enableSiblingPrerendering')
1890 + ? ['A component was suspended by an uncached promise.']
1891 + : []),
1892 + ]);
1893
1894 assertLog(['Async text requested [World]']);
1895
1880 - await act(() => resolveTextRequests('World'));
1896 + if (gate('enableSiblingPrerendering')) {
1897 + await expect(async () => {
1898 + await act(() => resolveTextRequests('World'));
1899 + }).toErrorDev(['A component was suspended by an uncached promise.']);
1900 + } else {
1901 + await act(() => resolveTextRequests('World'));
1902 + }
1903
1904 assertLog(['Hi', 'World']);
1905 expect(root).toMatchRenderedOutput('Hi World');
@@ -1913,16 +1935,26 @@ describe('ReactUse', () => {
1935
1936 await expect(async () => {
1937 await act(() => resolveTextRequests('Hi'));
1916 - }).toErrorDev(
1938 + }).toErrorDev([
1939 // We get this warning because the generator's promise themselves are not cached.
1940 'A component was suspended by an uncached promise. Creating ' +
1941 'promises inside a Client Component or hook is not yet ' +
1942 'supported, except via a Suspense-compatible library or framework.',
1921 - );
1943 +
1944 + ...(gate('enableSiblingPrerendering')
1945 + ? ['A component was suspended by an uncached promise.']
1946 + : []),
1947 + ]);
1948
1949 assertLog(['Async text requested [World]']);
1950
1925 - await act(() => resolveTextRequests('World'));
1951 + if (gate('enableSiblingPrerendering')) {
1952 + await expect(async () => {
1953 + await act(() => resolveTextRequests('World'));
1954 + }).toErrorDev(['A component was suspended by an uncached promise.']);
1955 + } else {
1956 + await act(() => resolveTextRequests('World'));
1957 + }
1958
1959 assertLog(['Hi', 'World']);
1960 expect(root).toMatchRenderedOutput(<div>Hi World</div>);
packages/react-reconciler/src/__tests__/useMemoCache-test.js
+28 -4
@@ -559,13 +559,26 @@ describe('useMemoCache()', () => {
559 root.render(<App chunkA={updatedChunkA} chunkB={updatedChunkB} />);
560 });
561 });
562 - assertLog(['Suspend! [chunkA]']);
562 + assertLog([
563 + 'Suspend! [chunkA]',
564 +
565 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [chunkA]'] : []),
566 + ]);
567
568 // The data starts to stream in. Loading the data in the first chunk
569 // triggers an expensive computation in the UI. Later, we'll test whether
570 // this computation is reused.
571 await act(() => updatedChunkA.resolve('A2'));
568 - assertLog(['Some expensive processing... [A2]', 'Suspend! [chunkB]']);
572 + assertLog([
573 + 'Some expensive processing... [A2]',
574 + 'Suspend! [chunkB]',
575 +
576 + ...(gate('enableSiblingPrerendering')
577 + ? gate('enableNoCloningMemoCache')
578 + ? ['Suspend! [chunkB]']
579 + : ['Some expensive processing... [A2]', 'Suspend! [chunkB]']
580 + : []),
581 + ]);
582
583 // The second chunk hasn't loaded yet, so we're still showing the
584 // initial UI.
@@ -586,11 +599,22 @@ describe('useMemoCache()', () => {
599 if (gate(flags => flags.enableNoCloningMemoCache)) {
600 // We did not have process the first chunk again. We reused the
601 // computation from the earlier attempt.
589 - assertLog(['Suspend! [chunkB]']);
602 + assertLog([
603 + 'Suspend! [chunkB]',
604 +
605 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [chunkB]'] : []),
606 + ]);
607 } else {
608 // Because we clone/reset the memo cache after every aborted attempt, we
609 // must process the first chunk again.
593 - assertLog(['Some expensive processing... [A2]', 'Suspend! [chunkB]']);
610 + assertLog([
611 + 'Some expensive processing... [A2]',
612 + 'Suspend! [chunkB]',
613 +
614 + ...(gate('enableSiblingPrerendering')
615 + ? ['Some expensive processing... [A2]', 'Suspend! [chunkB]']
616 + : []),
617 + ]);
618 }
619
620 expect(root).toMatchRenderedOutput(
packages/react-reconciler/src/__tests__/useSyncExternalStore-test.js
+4
@@ -274,6 +274,10 @@ describe('useSyncExternalStore', () => {
274 // This should a synchronous re-render of A using the updated value. In
275 // this test, this causes A to suspend.
276 'Suspend A',
277 +
278 + ...(gate('enableSiblingPrerendering')
279 + ? ['Suspend A', 'B: Updated']
280 + : []),
281 ]);
282 // Nothing has committed, because A suspended and no fallback
283 // was provided.