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.