@samitouri / QOS-React / commits / d6cb4e7713

Start prerendering Suspense retries immediately (#30934)

When a component suspends and is replaced by a fallback, we should start prerendering the fallback immediately, even before any new data is received. During the retry, we can enter prerender mode directly if we're sure that no new data was received since we last attempted to render the boundary. To do this, when completing the fallback, we leave behind a pending retry lane on the Suspense boundary. Previously we only did this once a promise resolved, but by assigning a lane during the complete phase, we will know that there's speculative work to be done. Then, upon committing the fallback, we mark the retry lane as suspended — but only if nothing was pinged or updated in the meantime. That allows us to immediately enter prerender mode (i.e. render without skipping any siblings) when performing the retry.

Andrew Clark committed Sep 11, 2024 at 11:41 UTC d6cb4e771341ff82489c00f4907990cb8a75696b
29 files changed +1906 -462
packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js
+53 -45
@@ -18,6 +18,7 @@ let Suspense;
18 let TextResource;
19 let textResourceShouldFail;
20 let waitForAll;
21 +let waitForPaint;
22 let assertLog;
23 let waitForThrow;
24 let act;
@@ -37,6 +38,7 @@ describe('ReactCache', () => {
38 waitForAll = InternalTestUtils.waitForAll;
39 assertLog = InternalTestUtils.assertLog;
40 waitForThrow = InternalTestUtils.waitForThrow;
41 + waitForPaint = InternalTestUtils.waitForPaint;
42 act = InternalTestUtils.act;
43
44 TextResource = createResource(
@@ -119,7 +121,12 @@ describe('ReactCache', () => {
121 const root = ReactNoop.createRoot();
122 root.render(<App />);
123
122 - await waitForAll(['Suspend! [Hi]', 'Loading...']);
124 + await waitForAll([
125 + 'Suspend! [Hi]',
126 + 'Loading...',
127 +
128 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [Hi]'] : []),
129 + ]);
130
131 jest.advanceTimersByTime(100);
132 assertLog(['Promise resolved [Hi]']);
@@ -138,7 +145,12 @@ describe('ReactCache', () => {
145 const root = ReactNoop.createRoot();
146 root.render(<App />);
147
141 - await waitForAll(['Suspend! [Hi]', 'Loading...']);
148 + await waitForAll([
149 + 'Suspend! [Hi]',
150 + 'Loading...',
151 +
152 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [Hi]'] : []),
153 + ]);
154
155 textResourceShouldFail = true;
156 let error;
@@ -148,15 +160,7 @@ describe('ReactCache', () => {
160 error = e;
161 }
162 expect(error.message).toMatch('Failed to load: Hi');
151 - assertLog([
152 - 'Promise rejected [Hi]',
153 - 'Error! [Hi]',
154 - 'Error! [Hi]',
155 -
156 - ...(gate('enableSiblingPrerendering')
157 - ? ['Error! [Hi]', 'Error! [Hi]']
158 - : []),
159 - ]);
163 + assertLog(['Promise rejected [Hi]', 'Error! [Hi]', 'Error! [Hi]']);
164
165 // Should throw again on a subsequent read
166 root.render(<App />);
@@ -187,15 +191,27 @@ describe('ReactCache', () => {
191
192 if (__DEV__) {
193 await expect(async () => {
190 - await waitForAll(['App', 'Loading...']);
194 + await waitForAll([
195 + 'App',
196 + 'Loading...',
197 +
198 + ...(gate('enableSiblingPrerendering') ? ['App'] : []),
199 + ]);
200 }).toErrorDev([
201 'Invalid key type. Expected a string, number, symbol, or ' +
202 "boolean, but instead received: [ 'Hi', 100 ]\n\n" +
203 'To use non-primitive values as keys, you must pass a hash ' +
204 'function as the second argument to createResource().',
205 +
206 + ...(gate('enableSiblingPrerendering') ? ['Invalid key type'] : []),
207 ]);
208 } else {
198 - await waitForAll(['App', 'Loading...']);
209 + await waitForAll([
210 + 'App',
211 + 'Loading...',
212 +
213 + ...(gate('enableSiblingPrerendering') ? ['App'] : []),
214 + ]);
215 }
216 });
217
@@ -212,13 +228,17 @@ describe('ReactCache', () => {
228 <AsyncText ms={100} text={3} />
229 </Suspense>,
230 );
215 - await waitForAll(['Suspend! [1]', 'Loading...']);
231 + await waitForPaint(['Suspend! [1]', 'Loading...']);
232 jest.advanceTimersByTime(100);
233 assertLog(['Promise resolved [1]']);
218 - await waitForAll([1, 'Suspend! [2]', 1, 'Suspend! [2]', 'Suspend! [3]']);
234 + await waitForAll([1, 'Suspend! [2]']);
235 +
236 + jest.advanceTimersByTime(100);
237 + assertLog(['Promise resolved [2]']);
238 + await waitForAll([1, 2, 'Suspend! [3]']);
239
240 jest.advanceTimersByTime(100);
221 - assertLog(['Promise resolved [2]', 'Promise resolved [3]']);
241 + assertLog(['Promise resolved [3]']);
242 await waitForAll([1, 2, 3]);
243
244 await act(() => jest.advanceTimersByTime(100));
@@ -233,25 +253,18 @@ describe('ReactCache', () => {
253 </Suspense>,
254 );
255
236 - await waitForAll([1, 'Suspend! [4]', 'Loading...']);
237 -
238 - await act(() => jest.advanceTimersByTime(100));
239 - assertLog([
240 - 'Promise resolved [4]',
241 -
256 + await waitForAll([
257 1,
243 - 4,
244 - 'Suspend! [5]',
258 + 'Suspend! [4]',
259 + 'Loading...',
260 1,
246 - 4,
261 + 'Suspend! [4]',
262 'Suspend! [5]',
248 -
249 - 'Promise resolved [5]',
250 - 1,
251 - 4,
252 - 5,
263 ]);
264
265 + await act(() => jest.advanceTimersByTime(100));
266 + assertLog(['Promise resolved [4]', 'Promise resolved [5]', 1, 4, 5]);
267 +
268 expect(root).toMatchRenderedOutput('145');
269
270 // We've now rendered values 1, 2, 3, 4, 5, over our limit of 3. The least
@@ -271,24 +284,14 @@ describe('ReactCache', () => {
284 // 2 and 3 suspend because they were evicted from the cache
285 'Suspend! [2]',
286 'Loading...',
274 - ]);
275 -
276 - await act(() => jest.advanceTimersByTime(100));
277 - assertLog([
278 - 'Promise resolved [2]',
287
288 1,
281 - 2,
282 - 'Suspend! [3]',
283 - 1,
284 - 2,
289 + 'Suspend! [2]',
290 'Suspend! [3]',
286 -
287 - 'Promise resolved [3]',
288 - 1,
289 - 2,
290 - 3,
291 ]);
292 +
293 + await act(() => jest.advanceTimersByTime(100));
294 + assertLog(['Promise resolved [2]', 'Promise resolved [3]', 1, 2, 3]);
295 expect(root).toMatchRenderedOutput('123');
296 });
297
@@ -363,7 +366,12 @@ describe('ReactCache', () => {
366 </Suspense>,
367 );
368
366 - await waitForAll(['Suspend! [Hi]', 'Loading...']);
369 + await waitForAll([
370 + 'Suspend! [Hi]',
371 + 'Loading...',
372 +
373 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [Hi]'] : []),
374 + ]);
375
376 resolveThenable('Hi');
377 // This thenable improperly resolves twice. We should not update the
packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js
+94 -10
@@ -15,6 +15,17 @@ import {
15 normalizeCodeLocInfo,
16 } from './utils';
17
18 +import {ReactVersion} from '../../../../ReactVersions';
19 +import semver from 'semver';
20 +
21 +// TODO: This is how other DevTools tests access the version but we should find
22 +// a better solution for this
23 +const ReactVersionTestingAgainst = process.env.REACT_VERSION || ReactVersion;
24 +const enableSiblingPrerendering = semver.gte(
25 + ReactVersionTestingAgainst,
26 + '19.0.0',
27 +);
28 +
29 describe('Timeline profiler', () => {
30 let React;
31 let Scheduler;
@@ -1651,7 +1662,11 @@ describe('Timeline profiler', () => {
1662 </React.Suspense>,
1663 );
1664
1654 - await waitForAll(['suspended']);
1665 + await waitForAll([
1666 + 'suspended',
1667 +
1668 + ...(enableSiblingPrerendering ? ['suspended'] : []),
1669 + ]);
1670
1671 Scheduler.unstable_advanceTime(10);
1672 resolveFn();
@@ -1662,9 +1677,38 @@ describe('Timeline profiler', () => {
1677 const timelineData = stopProfilingAndGetTimelineData();
1678
1679 // Verify the Suspense event and duration was recorded.
1665 - expect(timelineData.suspenseEvents).toHaveLength(1);
1666 - const suspenseEvent = timelineData.suspenseEvents[0];
1667 - expect(suspenseEvent).toMatchInlineSnapshot(`
1680 + if (enableSiblingPrerendering) {
1681 + expect(timelineData.suspenseEvents).toMatchInlineSnapshot(`
1682 + [
1683 + {
1684 + "componentName": "Example",
1685 + "depth": 0,
1686 + "duration": 10,
1687 + "id": "0",
1688 + "phase": "mount",
1689 + "promiseName": "",
1690 + "resolution": "resolved",
1691 + "timestamp": 10,
1692 + "type": "suspense",
1693 + "warning": null,
1694 + },
1695 + {
1696 + "componentName": "Example",
1697 + "depth": 0,
1698 + "duration": 10,
1699 + "id": "0",
1700 + "phase": "mount",
1701 + "promiseName": "",
1702 + "resolution": "resolved",
1703 + "timestamp": 10,
1704 + "type": "suspense",
1705 + "warning": null,
1706 + },
1707 + ]
1708 + `);
1709 + } else {
1710 + const suspenseEvent = timelineData.suspenseEvents[0];
1711 + expect(suspenseEvent).toMatchInlineSnapshot(`
1712 {
1713 "componentName": "Example",
1714 "depth": 0,
@@ -1678,10 +1722,13 @@ describe('Timeline profiler', () => {
1722 "warning": null,
1723 }
1724 `);
1725 + }
1726
1727 // There should be two batches of renders: Suspeneded and resolved.
1728 expect(timelineData.batchUIDToMeasuresMap.size).toBe(2);
1684 - expect(timelineData.componentMeasures).toHaveLength(2);
1729 + expect(timelineData.componentMeasures).toHaveLength(
1730 + enableSiblingPrerendering ? 3 : 2,
1731 + );
1732 });
1733
1734 it('should mark concurrent render with suspense that rejects', async () => {
@@ -1708,7 +1755,11 @@ describe('Timeline profiler', () => {
1755 </React.Suspense>,
1756 );
1757
1711 - await waitForAll(['suspended']);
1758 + await waitForAll([
1759 + 'suspended',
1760 +
1761 + ...(enableSiblingPrerendering ? ['suspended'] : []),
1762 + ]);
1763
1764 Scheduler.unstable_advanceTime(10);
1765 rejectFn();
@@ -1719,9 +1770,39 @@ describe('Timeline profiler', () => {
1770 const timelineData = stopProfilingAndGetTimelineData();
1771
1772 // Verify the Suspense event and duration was recorded.
1722 - expect(timelineData.suspenseEvents).toHaveLength(1);
1723 - const suspenseEvent = timelineData.suspenseEvents[0];
1724 - expect(suspenseEvent).toMatchInlineSnapshot(`
1773 + if (enableSiblingPrerendering) {
1774 + expect(timelineData.suspenseEvents).toMatchInlineSnapshot(`
1775 + [
1776 + {
1777 + "componentName": "Example",
1778 + "depth": 0,
1779 + "duration": 10,
1780 + "id": "0",
1781 + "phase": "mount",
1782 + "promiseName": "",
1783 + "resolution": "rejected",
1784 + "timestamp": 10,
1785 + "type": "suspense",
1786 + "warning": null,
1787 + },
1788 + {
1789 + "componentName": "Example",
1790 + "depth": 0,
1791 + "duration": 10,
1792 + "id": "0",
1793 + "phase": "mount",
1794 + "promiseName": "",
1795 + "resolution": "rejected",
1796 + "timestamp": 10,
1797 + "type": "suspense",
1798 + "warning": null,
1799 + },
1800 + ]
1801 + `);
1802 + } else {
1803 + expect(timelineData.suspenseEvents).toHaveLength(1);
1804 + const suspenseEvent = timelineData.suspenseEvents[0];
1805 + expect(suspenseEvent).toMatchInlineSnapshot(`
1806 {
1807 "componentName": "Example",
1808 "depth": 0,
@@ -1735,10 +1816,13 @@ describe('Timeline profiler', () => {
1816 "warning": null,
1817 }
1818 `);
1819 + }
1820
1821 // There should be two batches of renders: Suspeneded and resolved.
1822 expect(timelineData.batchUIDToMeasuresMap.size).toBe(2);
1741 - expect(timelineData.componentMeasures).toHaveLength(2);
1823 + expect(timelineData.componentMeasures).toHaveLength(
1824 + enableSiblingPrerendering ? 3 : 2,
1825 + );
1826 });
1827
1828 it('should mark cascading class component state updates', async () => {
packages/react-dom/src/__tests__/ReactDOMForm-test.js
+12 -2
@@ -1459,13 +1459,23 @@ describe('ReactDOMForm', () => {
1459 </Suspense>,
1460 ),
1461 );
1462 - assertLog(['Suspend! [Count: 0]', 'Loading...']);
1462 + assertLog([
1463 + 'Suspend! [Count: 0]',
1464 + 'Loading...',
1465 +
1466 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [Count: 0]'] : []),
1467 + ]);
1468 await act(() => resolveText('Count: 0'));
1469 assertLog(['Count: 0']);
1470
1471 // Dispatch outside of a transition. This will trigger a loading state.
1472 await act(() => dispatch());
1468 - assertLog(['Suspend! [Count: 1]', 'Loading...']);
1473 + assertLog([
1474 + 'Suspend! [Count: 1]',
1475 + 'Loading...',
1476 +
1477 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [Count: 1]'] : []),
1478 + ]);
1479 expect(container.textContent).toBe('Loading...');
1480
1481 await act(() => resolveText('Count: 1'));
packages/react-dom/src/__tests__/ReactDOMSuspensePlaceholder-test.js
+7 -1
@@ -160,7 +160,13 @@ describe('ReactDOMSuspensePlaceholder', () => {
160 });
161
162 expect(container.textContent).toEqual('Loading...');
163 - assertLog(['A', 'Suspend! [B]', 'Loading...']);
163 + assertLog([
164 + 'A',
165 + 'Suspend! [B]',
166 + 'Loading...',
167 +
168 + ...(gate('enableSiblingPrerendering') ? ['A', 'Suspend! [B]', 'C'] : []),
169 + ]);
170 await act(() => {
171 resolveText('B');
172 });
packages/react-dom/src/__tests__/ReactWrongReturnPointer-test.js
+9 -1
@@ -192,7 +192,13 @@ test('regression (#20932): return pointer is correct before entering deleted tre
192 await act(() => {
193 root.render(<App />);
194 });
195 - assertLog(['Suspend! [0]', 'Loading Async...', 'Loading Tail...']);
195 + assertLog([
196 + 'Suspend! [0]',
197 + 'Loading Async...',
198 + 'Loading Tail...',
199 +
200 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [0]'] : []),
201 + ]);
202 await act(() => {
203 resolveText(0);
204 });
@@ -205,5 +211,7 @@ test('regression (#20932): return pointer is correct before entering deleted tre
211 'Loading Async...',
212 'Suspend! [1]',
213 'Loading Async...',
214 +
215 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [1]'] : []),
216 ]);
217 });
packages/react-reconciler/src/ReactFiberCompleteWork.js
+23 -19
@@ -155,6 +155,7 @@ import {
155 getRenderTargetTime,
156 getWorkInProgressTransitions,
157 shouldRemainOnPreviousScreen,
158 + markSpawnedRetryLane,
159 } from './ReactFiberWorkLoop';
160 import {
161 OffscreenLane,
@@ -600,25 +601,28 @@ function scheduleRetryEffect(
601 // Schedule an effect to attach a retry listener to the promise.
602 // TODO: Move to passive phase
603 workInProgress.flags |= Update;
603 - } else {
604 - // This boundary suspended, but no wakeables were added to the retry
605 - // queue. Check if the renderer suspended commit. If so, this means
606 - // that once the fallback is committed, we can immediately retry
607 - // rendering again, because rendering wasn't actually blocked. Only
608 - // the commit phase.
609 - // TODO: Consider a model where we always schedule an immediate retry, even
610 - // for normal Suspense. That way the retry can partially render up to the
611 - // first thing that suspends.
612 - if (workInProgress.flags & ScheduleRetry) {
613 - const retryLane =
614 - // TODO: This check should probably be moved into claimNextRetryLane
615 - // I also suspect that we need some further consolidation of offscreen
616 - // and retry lanes.
617 - workInProgress.tag !== OffscreenComponent
618 - ? claimNextRetryLane()
619 - : OffscreenLane;
620 - workInProgress.lanes = mergeLanes(workInProgress.lanes, retryLane);
621 - }
604 + }
605 +
606 + // Check if we need to schedule an immediate retry. This should happen
607 + // whenever we unwind a suspended tree without fully rendering its siblings;
608 + // we need to begin the retry so we can start prerendering them.
609 + //
610 + // We also use this mechanism for Suspensey Resources (e.g. stylesheets),
611 + // because those don't actually block the render phase, only the commit phase.
612 + // So we can start rendering even before the resources are ready.
613 + if (workInProgress.flags & ScheduleRetry) {
614 + const retryLane =
615 + // TODO: This check should probably be moved into claimNextRetryLane
616 + // I also suspect that we need some further consolidation of offscreen
617 + // and retry lanes.
618 + workInProgress.tag !== OffscreenComponent
619 + ? claimNextRetryLane()
620 + : OffscreenLane;
621 + workInProgress.lanes = mergeLanes(workInProgress.lanes, retryLane);
622 +
623 + // Track the lanes that have been scheduled for an immediate retry so that
624 + // we can mark them as suspended upon committing the root.
625 + markSpawnedRetryLane(retryLane);
626 }
627 }
628
packages/react-reconciler/src/ReactFiberLane.js
+71 -7
@@ -26,9 +26,11 @@ import {
26 syncLaneExpirationMs,
27 transitionLaneExpirationMs,
28 retryLaneExpirationMs,
29 + disableLegacyMode,
30 } from 'shared/ReactFeatureFlags';
31 import {isDevToolsPresent} from './ReactFiberDevToolsHook';
32 import {clz32} from './clz32';
33 +import {LegacyRoot} from './ReactRootTags';
34
35 // Lane values below should be kept in sync with getLabelForLane(), used by react-devtools-timeline.
36 // If those values are changed that package should be rebuilt and redeployed.
@@ -231,6 +233,29 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
233 const pingedLanes = root.pingedLanes;
234 const warmLanes = root.warmLanes;
235
236 + // finishedLanes represents a completed tree that is ready to commit.
237 + //
238 + // It's not worth doing discarding the completed tree in favor of performing
239 + // speculative work. So always check this before deciding to warm up
240 + // the siblings.
241 + //
242 + // Note that this is not set in a "suspend indefinitely" scenario, like when
243 + // suspending outside of a Suspense boundary, or in the shell during a
244 + // transition — only in cases where we are very likely to commit the tree in
245 + // a brief amount of time (i.e. below the "Just Noticeable Difference"
246 + // threshold).
247 + //
248 + // TODO: finishedLanes is also set when a Suspensey resource, like CSS or
249 + // images, suspends during the commit phase. (We could detect that here by
250 + // checking for root.cancelPendingCommit.) These are also expected to resolve
251 + // quickly, because of preloading, but theoretically they could block forever
252 + // like in a normal "suspend indefinitely" scenario. In the future, we should
253 + // consider only blocking for up to some time limit before discarding the
254 + // commit in favor of prerendering. If we do discard a pending commit, then
255 + // the commit phase callback should act as a ping to try the original
256 + // render again.
257 + const rootHasPendingCommit = root.finishedLanes !== NoLanes;
258 +
259 // Do not work on any idle work until all the non-idle work has finished,
260 // even if the work is suspended.
261 const nonIdlePendingLanes = pendingLanes & NonIdleLanes;
@@ -246,9 +271,11 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
271 nextLanes = getHighestPriorityLanes(nonIdlePingedLanes);
272 } else {
273 // 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);
274 + if (!rootHasPendingCommit) {
275 + const lanesToPrewarm = nonIdlePendingLanes & ~warmLanes;
276 + if (lanesToPrewarm !== NoLanes) {
277 + nextLanes = getHighestPriorityLanes(lanesToPrewarm);
278 + }
279 }
280 }
281 }
@@ -268,9 +295,11 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
295 nextLanes = getHighestPriorityLanes(pingedLanes);
296 } else {
297 // 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);
298 + if (!rootHasPendingCommit) {
299 + const lanesToPrewarm = pendingLanes & ~warmLanes;
300 + if (lanesToPrewarm !== NoLanes) {
301 + nextLanes = getHighestPriorityLanes(lanesToPrewarm);
302 + }
303 }
304 }
305 }
@@ -753,10 +782,14 @@ export function markRootPinged(root: FiberRoot, pingedLanes: Lanes) {
782
783 export function markRootFinished(
784 root: FiberRoot,
785 + finishedLanes: Lanes,
786 remainingLanes: Lanes,
787 spawnedLane: Lane,
788 + updatedLanes: Lanes,
789 + suspendedRetryLanes: Lanes,
790 ) {
759 - const noLongerPendingLanes = root.pendingLanes & ~remainingLanes;
791 + const previouslyPendingLanes = root.pendingLanes;
792 + const noLongerPendingLanes = previouslyPendingLanes & ~remainingLanes;
793
794 root.pendingLanes = remainingLanes;
795
@@ -812,6 +845,37 @@ export function markRootFinished(
845 NoLanes,
846 );
847 }
848 +
849 + // suspendedRetryLanes represents the retry lanes spawned by new Suspense
850 + // boundaries during this render that were not later pinged.
851 + //
852 + // These lanes were marked as pending on their associated Suspense boundary
853 + // fiber during the render phase so that we could start rendering them
854 + // before new data streams in. As soon as the fallback commits, we can try
855 + // to render them again.
856 + //
857 + // But since we know they're still suspended, we can skip straight to the
858 + // "prerender" mode (i.e. don't skip over siblings after something
859 + // suspended) instead of the regular mode (i.e. unwind and skip the siblings
860 + // as soon as something suspends to unblock the rest of the update).
861 + if (
862 + suspendedRetryLanes !== NoLanes &&
863 + // Note that we only do this if there were no updates since we started
864 + // rendering. This mirrors the logic in markRootUpdated — whenever we
865 + // receive an update, we reset all the suspended and pinged lanes.
866 + updatedLanes === NoLanes &&
867 + !(disableLegacyMode && root.tag === LegacyRoot)
868 + ) {
869 + // We also need to avoid marking a retry lane as suspended if it was already
870 + // pending before this render. We can't say these are now suspended if they
871 + // weren't included in our attempt.
872 + const freshlySpawnedRetryLanes =
873 + suspendedRetryLanes &
874 + // Remove any retry lane that was already pending before our just-finished
875 + // attempt, and also wasn't included in that attempt.
876 + ~(previouslyPendingLanes & ~finishedLanes);
877 + root.suspendedLanes |= freshlySpawnedRetryLanes;
878 + }
879 }
880
881 function markSpawnedDeferredLane(
packages/react-reconciler/src/ReactFiberWorkLoop.js
+134 -13
@@ -128,6 +128,7 @@ import {
128 DidDefer,
129 ShouldSuspendCommit,
130 MaySuspendCommit,
131 + ScheduleRetry,
132 } from './ReactFiberFlags';
133 import {
134 NoLanes,
@@ -365,8 +366,11 @@ let workInProgressRootInterleavedUpdatedLanes: Lanes = NoLanes;
366 let workInProgressRootRenderPhaseUpdatedLanes: Lanes = NoLanes;
367 // Lanes that were pinged (in an interleaved event) during this render.
368 let workInProgressRootPingedLanes: Lanes = NoLanes;
368 -// If this lane scheduled deferred work, this is the lane of the deferred task.
369 +// If this render scheduled deferred work, this is the lane of the deferred task.
370 let workInProgressDeferredLane: Lane = NoLane;
371 +// Represents the retry lanes that were spawned by this render and have not
372 +// been pinged since, implying that they are still suspended.
373 +let workInProgressSuspendedRetryLanes: Lanes = NoLanes;
374 // Errors that are thrown during the render phase.
375 let workInProgressRootConcurrentErrors: Array<CapturedValue<mixed>> | null =
376 null;
@@ -993,8 +997,6 @@ export function performConcurrentWorkOnRoot(
997
998 // We now have a consistent tree. The next step is either to commit it,
999 // or, if something suspended, wait to commit it after a timeout.
996 - root.finishedWork = finishedWork;
997 - root.finishedLanes = lanes;
1000 finishConcurrentRender(root, exitStatus, finishedWork, lanes);
1001 }
1002 break;
@@ -1138,6 +1140,12 @@ function finishConcurrentRender(
1140 }
1141 }
1142
1143 + // Only set these if we have a complete tree that is ready to be committed.
1144 + // We use these fields to determine later whether or not the work should be
1145 + // discarded for a fresh render attempt.
1146 + root.finishedWork = finishedWork;
1147 + root.finishedLanes = lanes;
1148 +
1149 if (shouldForceFlushFallbacksInDEV()) {
1150 // We're inside an `act` scope. Commit immediately.
1151 commitRoot(
@@ -1146,6 +1154,8 @@ function finishConcurrentRender(
1154 workInProgressTransitions,
1155 workInProgressRootDidIncludeRecursiveRenderUpdate,
1156 workInProgressDeferredLane,
1157 + workInProgressRootInterleavedUpdatedLanes,
1158 + workInProgressSuspendedRetryLanes,
1159 );
1160 } else {
1161 if (
@@ -1188,6 +1198,8 @@ function finishConcurrentRender(
1198 workInProgressRootDidIncludeRecursiveRenderUpdate,
1199 lanes,
1200 workInProgressDeferredLane,
1201 + workInProgressRootInterleavedUpdatedLanes,
1202 + workInProgressSuspendedRetryLanes,
1203 workInProgressRootDidSkipSuspendedSiblings,
1204 ),
1205 msUntilTimeout,
@@ -1203,6 +1215,8 @@ function finishConcurrentRender(
1215 workInProgressRootDidIncludeRecursiveRenderUpdate,
1216 lanes,
1217 workInProgressDeferredLane,
1218 + workInProgressRootInterleavedUpdatedLanes,
1219 + workInProgressSuspendedRetryLanes,
1220 workInProgressRootDidSkipSuspendedSiblings,
1221 );
1222 }
@@ -1216,6 +1230,8 @@ function commitRootWhenReady(
1230 didIncludeRenderPhaseUpdate: boolean,
1231 lanes: Lanes,
1232 spawnedLane: Lane,
1233 + updatedLanes: Lanes,
1234 + suspendedRetryLanes: Lanes,
1235 didSkipSuspendedSiblings: boolean,
1236 ) {
1237 // TODO: Combine retry throttling with Suspensey commits. Right now they run
@@ -1254,6 +1270,9 @@ function commitRootWhenReady(
1270 recoverableErrors,
1271 transitions,
1272 didIncludeRenderPhaseUpdate,
1273 + spawnedLane,
1274 + updatedLanes,
1275 + suspendedRetryLanes,
1276 ),
1277 );
1278 markRootSuspended(root, lanes, spawnedLane, didSkipSuspendedSiblings);
@@ -1261,13 +1280,15 @@ function commitRootWhenReady(
1280 }
1281 }
1282
1264 - // Otherwise, commit immediately.
1283 + // Otherwise, commit immediately.;
1284 commitRoot(
1285 root,
1286 recoverableErrors,
1287 transitions,
1288 didIncludeRenderPhaseUpdate,
1289 spawnedLane,
1290 + updatedLanes,
1291 + suspendedRetryLanes,
1292 );
1293 }
1294
@@ -1277,7 +1298,13 @@ function isRenderConsistentWithExternalStores(finishedWork: Fiber): boolean {
1298 // loop instead of recursion so we can exit early.
1299 let node: Fiber = finishedWork;
1300 while (true) {
1280 - if (node.flags & StoreConsistency) {
1301 + const tag = node.tag;
1302 + if (
1303 + (tag === FunctionComponent ||
1304 + tag === ForwardRef ||
1305 + tag === SimpleMemoComponent) &&
1306 + node.flags & StoreConsistency
1307 + ) {
1308 const updateQueue: FunctionComponentUpdateQueue | null =
1309 (node.updateQueue: any);
1310 if (updateQueue !== null) {
@@ -1464,6 +1491,8 @@ export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null {
1491 workInProgressTransitions,
1492 workInProgressRootDidIncludeRecursiveRenderUpdate,
1493 workInProgressDeferredLane,
1494 + workInProgressRootInterleavedUpdatedLanes,
1495 + workInProgressSuspendedRetryLanes,
1496 );
1497
1498 // Before exiting, make sure there's a callback scheduled for the next
@@ -1691,6 +1720,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1720 workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
1721 workInProgressRootPingedLanes = NoLanes;
1722 workInProgressDeferredLane = NoLane;
1723 + workInProgressSuspendedRetryLanes = NoLanes;
1724 workInProgressRootConcurrentErrors = null;
1725 workInProgressRootRecoverableErrors = null;
1726 workInProgressRootDidIncludeRecursiveRenderUpdate = false;
@@ -2104,9 +2134,10 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
2134 }
2135 default: {
2136 // Unwind then continue with the normal work loop.
2137 + const reason = workInProgressSuspendedReason;
2138 workInProgressSuspendedReason = NotSuspended;
2139 workInProgressThrownValue = null;
2109 - throwAndUnwindWorkLoop(root, unitOfWork, thrownValue);
2140 + throwAndUnwindWorkLoop(root, unitOfWork, thrownValue, reason);
2141 break;
2142 }
2143 }
@@ -2199,6 +2230,14 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
2230 workInProgressTransitions = getTransitionsForLanes(root, lanes);
2231 resetRenderTimer();
2232 prepareFreshStack(root, lanes);
2233 + } else {
2234 + // This is a continuation of an existing work-in-progress.
2235 + //
2236 + // If we were previously in prerendering mode, check if we received any new
2237 + // data during an interleaved event.
2238 + if (workInProgressRootIsPrerendering) {
2239 + workInProgressRootIsPrerendering = checkIfRootIsPrerendering(root, lanes);
2240 + }
2241 }
2242
2243 if (__DEV__) {
@@ -2226,7 +2265,12 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
2265 // Unwind then continue with the normal work loop.
2266 workInProgressSuspendedReason = NotSuspended;
2267 workInProgressThrownValue = null;
2229 - throwAndUnwindWorkLoop(root, unitOfWork, thrownValue);
2268 + throwAndUnwindWorkLoop(
2269 + root,
2270 + unitOfWork,
2271 + thrownValue,
2272 + SuspendedOnError,
2273 + );
2274 break;
2275 }
2276 case SuspendedOnData: {
@@ -2284,7 +2328,12 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
2328 // Otherwise, unwind then continue with the normal work loop.
2329 workInProgressSuspendedReason = NotSuspended;
2330 workInProgressThrownValue = null;
2287 - throwAndUnwindWorkLoop(root, unitOfWork, thrownValue);
2331 + throwAndUnwindWorkLoop(
2332 + root,
2333 + unitOfWork,
2334 + thrownValue,
2335 + SuspendedAndReadyToContinue,
2336 + );
2337 }
2338 break;
2339 }
@@ -2347,7 +2396,12 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
2396 // Otherwise, unwind then continue with the normal work loop.
2397 workInProgressSuspendedReason = NotSuspended;
2398 workInProgressThrownValue = null;
2350 - throwAndUnwindWorkLoop(root, unitOfWork, thrownValue);
2399 + throwAndUnwindWorkLoop(
2400 + root,
2401 + unitOfWork,
2402 + thrownValue,
2403 + SuspendedOnInstanceAndReadyToContinue,
2404 + );
2405 break;
2406 }
2407 case SuspendedOnDeprecatedThrowPromise: {
@@ -2357,7 +2411,12 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
2411 // always unwind.
2412 workInProgressSuspendedReason = NotSuspended;
2413 workInProgressThrownValue = null;
2360 - throwAndUnwindWorkLoop(root, unitOfWork, thrownValue);
2414 + throwAndUnwindWorkLoop(
2415 + root,
2416 + unitOfWork,
2417 + thrownValue,
2418 + SuspendedOnDeprecatedThrowPromise,
2419 + );
2420 break;
2421 }
2422 case SuspendedOnHydration: {
@@ -2611,6 +2670,7 @@ function throwAndUnwindWorkLoop(
2670 root: FiberRoot,
2671 unitOfWork: Fiber,
2672 thrownValue: mixed,
2673 + suspendedReason: SuspendedReason,
2674 ) {
2675 // This is a fork of performUnitOfWork specifcally for unwinding a fiber
2676 // that threw an exception.
@@ -2658,16 +2718,43 @@ function throwAndUnwindWorkLoop(
2718 // The current algorithm for both hydration and error handling assumes
2719 // that the tree is rendered sequentially. So we always skip the siblings.
2720 getIsHydrating() ||
2661 - workInProgressSuspendedReason === SuspendedOnError
2721 + suspendedReason === SuspendedOnError
2722 ) {
2723 skipSiblings = true;
2724 // We intentionally don't set workInProgressRootDidSkipSuspendedSiblings,
2725 // because we don't want to trigger another prerender attempt.
2666 - } else if (!workInProgressRootIsPrerendering) {
2726 + } else if (
2727 + // Check whether this is a prerender
2728 + !workInProgressRootIsPrerendering &&
2729 + // Offscreen rendering is also a form of speculative rendering
2730 + !includesSomeLane(workInProgressRootRenderLanes, OffscreenLane)
2731 + ) {
2732 // This is not a prerender. Skip the siblings during this render. A
2733 // separate prerender will be scheduled for later.
2734 skipSiblings = true;
2735 workInProgressRootDidSkipSuspendedSiblings = true;
2736 +
2737 + // Because we're skipping the siblings, schedule an immediate retry of
2738 + // this boundary.
2739 + //
2740 + // The reason we do this is because a prerender is only scheduled when
2741 + // the root is blocked from committing, i.e. RootSuspendedWithDelay.
2742 + // When the root is not blocked, as in the case when we render a
2743 + // fallback, the original lane is considered to be finished, and
2744 + // therefore no longer in need of being prerendered. However, there's
2745 + // still a pending retry that will happen once the data streams in.
2746 + // We should start rendering that even before the data streams in so we
2747 + // can prerender the siblings.
2748 + if (
2749 + suspendedReason === SuspendedOnData ||
2750 + suspendedReason === SuspendedOnImmediate ||
2751 + suspendedReason === SuspendedOnDeprecatedThrowPromise
2752 + ) {
2753 + const boundary = getSuspenseHandler();
2754 + if (boundary !== null && boundary.tag === SuspenseComponent) {
2755 + boundary.flags |= ScheduleRetry;
2756 + }
2757 + }
2758 } else {
2759 // This is a prerender. Don't skip the siblings.
2760 skipSiblings = false;
@@ -2688,6 +2775,16 @@ function throwAndUnwindWorkLoop(
2775 }
2776 }
2777
2778 +export function markSpawnedRetryLane(lane: Lane): void {
2779 + // Keep track of the retry lanes that were spawned by a fallback during the
2780 + // current render and were not later pinged. This will represent the lanes
2781 + // that are known to still be suspended.
2782 + workInProgressSuspendedRetryLanes = mergeLanes(
2783 + workInProgressSuspendedRetryLanes,
2784 + lane,
2785 + );
2786 +}
2787 +
2788 function panicOnRootError(root: FiberRoot, error: mixed) {
2789 // There's no ancestor that can handle this exception. This should never
2790 // happen because the root is supposed to capture all errors that weren't
@@ -2866,6 +2963,8 @@ function commitRoot(
2963 transitions: Array<Transition> | null,
2964 didIncludeRenderPhaseUpdate: boolean,
2965 spawnedLane: Lane,
2966 + updatedLanes: Lanes,
2967 + suspendedRetryLanes: Lanes,
2968 ) {
2969 // TODO: This no longer makes any sense. We already wrap the mutation and
2970 // layout phases. Should be able to remove.
@@ -2881,6 +2980,8 @@ function commitRoot(
2980 didIncludeRenderPhaseUpdate,
2981 previousUpdateLanePriority,
2982 spawnedLane,
2983 + updatedLanes,
2984 + suspendedRetryLanes,
2985 );
2986 } finally {
2987 ReactSharedInternals.T = prevTransition;
@@ -2897,6 +2998,8 @@ function commitRootImpl(
2998 didIncludeRenderPhaseUpdate: boolean,
2999 renderPriorityLevel: EventPriority,
3000 spawnedLane: Lane,
3001 + updatedLanes: Lanes,
3002 + suspendedRetryLanes: Lanes,
3003 ) {
3004 do {
3005 // `flushPassiveEffects` will call `flushSyncUpdateQueue` at the end, which
@@ -2973,7 +3076,14 @@ function commitRootImpl(
3076 const concurrentlyUpdatedLanes = getConcurrentlyUpdatedLanes();
3077 remainingLanes = mergeLanes(remainingLanes, concurrentlyUpdatedLanes);
3078
2976 - markRootFinished(root, remainingLanes, spawnedLane);
3079 + markRootFinished(
3080 + root,
3081 + lanes,
3082 + remainingLanes,
3083 + spawnedLane,
3084 + updatedLanes,
3085 + suspendedRetryLanes,
3086 + );
3087
3088 // Reset this before firing side effects so we can detect recursive updates.
3089 didIncludeCommitPhaseUpdate = false;
@@ -3679,6 +3789,17 @@ function pingSuspendedRoot(
3789 pingedLanes,
3790 );
3791 }
3792 +
3793 + // If something pings the work-in-progress render, any work that suspended
3794 + // up to this point may now be unblocked; in other words, no
3795 + // longer suspended.
3796 + //
3797 + // Unlike the broader check above, we only need do this if the lanes match
3798 + // exactly. If the lanes don't exactly match, that implies the promise
3799 + // was created by an older render.
3800 + if (workInProgressSuspendedRetryLanes === workInProgressRootRenderLanes) {
3801 + workInProgressSuspendedRetryLanes = NoLanes;
3802 + }
3803 }
3804
3805 ensureRootIsScheduled(root);
packages/react-reconciler/src/__tests__/ActivityStrictMode-test.js
+5
@@ -240,6 +240,11 @@ describe('Activity StrictMode', () => {
240 'Parent mount',
241 'Parent unmount',
242 'Parent mount',
243 +
244 + ...(gate('enableSiblingPrerendering')
245 + ? ['Child rendered', 'Child suspended']
246 + : []),
247 +
248 '------------------------------',
249 'Child rendered',
250 'Child rendered',
packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js
+15 -1
@@ -187,12 +187,26 @@ describe('DebugTracing', () => {
187 `group: ⚛ render (${DEFAULT_LANE_STRING})`,
188 'log: ⚛ Example suspended',
189 `groupEnd: ⚛ render (${DEFAULT_LANE_STRING})`,
190 +
191 + ...(gate('enableSiblingPrerendering')
192 + ? [
193 + `group: ⚛ render (${RETRY_LANE_STRING})`,
194 + 'log: ⚛ Example suspended',
195 + `groupEnd: ⚛ render (${RETRY_LANE_STRING})`,
196 + ]
197 + : []),
198 ]);
199
200 logs.splice(0);
201
202 await act(async () => await resolveFakeSuspensePromise());
195 - expect(logs).toEqual(['log: ⚛ Example resolved']);
203 + expect(logs).toEqual([
204 + 'log: ⚛ Example resolved',
205 +
206 + ...(gate('enableSiblingPrerendering')
207 + ? ['log: ⚛ Example resolved']
208 + : []),
209 + ]);
210 });
211
212 // @gate experimental && build === 'development' && enableDebugTracing && enableCPUSuspense
packages/react-reconciler/src/__tests__/ReactActWarnings-test.js
+13 -3
@@ -313,13 +313,23 @@ describe('act warnings', () => {
313 act(() => {
314 root.render(<App />);
315 });
316 - assertLog(['Suspend! [Async]', 'Loading...']);
316 + assertLog([
317 + 'Suspend! [Async]',
318 + 'Loading...',
319 +
320 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [Async]'] : []),
321 + ]);
322 expect(root).toMatchRenderedOutput('Loading...');
323
324 // This is a retry, not a ping, because we already showed a fallback.
325 expect(() => resolveText('Async')).toErrorDev(
321 - 'A suspended resource finished loading inside a test, but the event ' +
322 - 'was not wrapped in act(...)',
326 + [
327 + 'A suspended resource finished loading inside a test, but the event ' +
328 + 'was not wrapped in act(...)',
329 +
330 + ...(gate('enableSiblingPrerendering') ? ['not wrapped in act'] : []),
331 + ],
332 +
333 {withoutStack: true},
334 );
335 });
packages/react-reconciler/src/__tests__/ReactBatching-test.internal.js
+7 -1
@@ -109,7 +109,13 @@ describe('ReactBlockingMode', () => {
109 </Suspense>,
110 );
111
112 - await waitForAll(['A', 'Suspend! [B]', 'Loading...']);
112 + await waitForAll([
113 + 'A',
114 + 'Suspend! [B]',
115 + 'Loading...',
116 +
117 + ...(gate('enableSiblingPrerendering') ? ['A', 'Suspend! [B]', 'C'] : []),
118 + ]);
119 // In Legacy Mode, A and B would mount in a hidden primary tree. In
120 // Concurrent Mode, nothing in the primary tree should mount. But the
121 // fallback should mount immediately.
packages/react-reconciler/src/__tests__/ReactConcurrentErrorRecovery-test.js
+1 -10
@@ -292,16 +292,7 @@ describe('ReactConcurrentErrorRecovery', () => {
292
293 // Because we're still suspended on B, we can't show an error boundary. We
294 // should wait for B to resolve.
295 - assertLog([
296 - 'Error! [A2]',
297 - 'Oops!',
298 - 'Suspend! [B2]',
299 - 'Loading...',
300 -
301 - ...(gate('enableSiblingPrerendering')
302 - ? ['Error! [A2]', 'Oops!', 'Suspend! [B2]', 'Loading...']
303 - : []),
304 - ]);
295 + assertLog(['Error! [A2]', 'Oops!', 'Suspend! [B2]', 'Loading...']);
296 // Remain on previous screen.
297 expect(root).toMatchRenderedOutput('A1B1');
298
packages/react-reconciler/src/__tests__/ReactContextPropagation-test.js
+20 -3
@@ -399,7 +399,13 @@ describe('ReactLazyContextPropagation', () => {
399 // the fallback displays despite this being a refresh.
400 setContext('B');
401 });
402 - assertLog(['Suspend! [B]', 'Loading...', 'B']);
402 + assertLog([
403 + 'Suspend! [B]',
404 + 'Loading...',
405 + 'B',
406 +
407 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [B]'] : []),
408 + ]);
409 expect(root).toMatchRenderedOutput('Loading...B');
410
411 await act(async () => {
@@ -479,7 +485,13 @@ describe('ReactLazyContextPropagation', () => {
485 // the fallback displays despite this being a refresh.
486 setContext('B');
487 });
482 - assertLog(['Suspend! [B]', 'Loading...', 'B']);
488 + assertLog([
489 + 'Suspend! [B]',
490 + 'Loading...',
491 + 'B',
492 +
493 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [B]'] : []),
494 + ]);
495 expect(root).toMatchRenderedOutput('Loading...B');
496
497 await act(async () => {
@@ -812,7 +824,12 @@ describe('ReactLazyContextPropagation', () => {
824 await act(() => {
825 setContext('B');
826 });
815 - assertLog(['Suspend! [B]', 'Loading...']);
827 + assertLog([
828 + 'Suspend! [B]',
829 + 'Loading...',
830 +
831 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [B]'] : []),
832 + ]);
833 expect(root).toMatchRenderedOutput('Loading...');
834
835 await act(async () => {
packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js
+4
@@ -499,6 +499,8 @@ describe('ReactDeferredValue', () => {
499 // The initial value suspended, so we attempt the final value, which
500 // also suspends.
501 'Suspend! [Final]',
502 +
503 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [Final]'] : []),
504 ]);
505 expect(root).toMatchRenderedOutput('Fallback');
506
@@ -630,6 +632,8 @@ describe('ReactDeferredValue', () => {
632 // go straight to attempting the final value.
633 'Suspend! [Content]',
634 'Loading...',
635 +
636 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [Content]'] : []),
637 ]);
638 // The content suspended, so we show a Suspense fallback
639 expect(root).toMatchRenderedOutput('Loading...');
packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
+21 -15
@@ -1864,13 +1864,15 @@ describe('ReactHooks', () => {
1864 it('does not fire a false positive warning when suspending memo', async () => {
1865 const {Suspense, useState} = React;
1866
1867 - let wasSuspended = false;
1867 + let isSuspended = true;
1868 let resolve;
1869 function trySuspend() {
1870 - if (!wasSuspended) {
1871 - throw new Promise(r => {
1872 - wasSuspended = true;
1873 - resolve = r;
1870 + if (isSuspended) {
1871 + throw new Promise(res => {
1872 + resolve = () => {
1873 + isSuspended = false;
1874 + res();
1875 + };
1876 });
1877 }
1878 }
@@ -1900,13 +1902,15 @@ describe('ReactHooks', () => {
1902 it('does not fire a false positive warning when suspending forwardRef', async () => {
1903 const {Suspense, useState} = React;
1904
1903 - let wasSuspended = false;
1905 + let isSuspended = true;
1906 let resolve;
1907 function trySuspend() {
1906 - if (!wasSuspended) {
1907 - throw new Promise(r => {
1908 - wasSuspended = true;
1909 - resolve = r;
1908 + if (isSuspended) {
1909 + throw new Promise(res => {
1910 + resolve = () => {
1911 + isSuspended = false;
1912 + res();
1913 + };
1914 });
1915 }
1916 }
@@ -1936,13 +1940,15 @@ describe('ReactHooks', () => {
1940 it('does not fire a false positive warning when suspending memo(forwardRef)', async () => {
1941 const {Suspense, useState} = React;
1942
1939 - let wasSuspended = false;
1943 + let isSuspended = true;
1944 let resolve;
1945 function trySuspend() {
1942 - if (!wasSuspended) {
1943 - throw new Promise(r => {
1944 - wasSuspended = true;
1945 - resolve = r;
1946 + if (isSuspended) {
1947 + throw new Promise(res => {
1948 + resolve = () => {
1949 + isSuspended = false;
1950 + res();
1951 + };
1952 });
1953 }
1954 }
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
+8 -8
@@ -3544,7 +3544,13 @@ describe('ReactHooksWithNoopRenderer', () => {
3544 ReactNoop.render(<App />);
3545 });
3546
3547 - assertLog(['A', 'Suspend! [A]', 'Loading']);
3547 + assertLog([
3548 + 'A',
3549 + 'Suspend! [A]',
3550 + 'Loading',
3551 +
3552 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [A]'] : []),
3553 + ]);
3554 expect(ReactNoop).toMatchRenderedOutput(
3555 <>
3556 <span prop="A" />
@@ -4201,13 +4207,7 @@ describe('ReactHooksWithNoopRenderer', () => {
4207 await act(async () => {
4208 await resolveText('A');
4209 });
4204 - assertLog([
4205 - 'Promise resolved [A]',
4206 - 'A',
4207 - 'Suspend! [B]',
4208 -
4209 - ...(gate('enableSiblingPrerendering') ? ['A', 'Suspend! [B]'] : []),
4210 - ]);
4210 + assertLog(['Promise resolved [A]', 'A', 'Suspend! [B]']);
4211
4212 await act(() => {
4213 root.render(null);
packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
+31 -18
@@ -198,11 +198,7 @@ describe('ReactLazy', () => {
198
199 await resolveFakeImport(Foo);
200
201 - await waitForAll([
202 - 'Foo',
203 -
204 - ...(gate('enableSiblingPrerendering') ? ['Foo'] : []),
205 - ]);
201 + await waitForAll(['Foo']);
202 expect(root).not.toMatchRenderedOutput('FooBar');
203
204 await act(() => resolveFakeImport(Bar));
@@ -239,13 +235,6 @@ describe('ReactLazy', () => {
235 assertConsoleErrorDev([
236 'Expected the result of a dynamic import() call',
237 '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 - : []),
238 ]);
239 expect(root).not.toMatchRenderedOutput('Hi');
240 });
@@ -320,7 +309,12 @@ describe('ReactLazy', () => {
309 unstable_isConcurrent: true,
310 });
311
323 - await waitForAll(['Suspend! [LazyChildA]', 'Loading...']);
312 + await waitForAll([
313 + 'Suspend! [LazyChildA]',
314 + 'Loading...',
315 +
316 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [LazyChildB]'] : []),
317 + ]);
318 expect(root).not.toMatchRenderedOutput('AB');
319
320 await act(async () => {
@@ -329,9 +323,23 @@ describe('ReactLazy', () => {
323 // B suspends even though it happens to share the same import as A.
324 // TODO: React.lazy should implement the `status` and `value` fields, so
325 // we can unwrap the result synchronously if it already loaded. Like `use`.
332 - await waitFor(['A', 'Suspend! [LazyChildB]']);
326 + await waitFor([
327 + 'A',
328 +
329 + // When enableSiblingPrerendering is on, LazyChildB was already
330 + // initialized. So it also already resolved when we called
331 + // resolveFakeImport above. So it doesn't suspend again.
332 + ...(gate('enableSiblingPrerendering')
333 + ? ['B']
334 + : ['Suspend! [LazyChildB]']),
335 + ]);
336 });
334 - assertLog(['A', 'B', 'Did mount: A', 'Did mount: B']);
337 + assertLog([
338 + ...(gate('enableSiblingPrerendering') ? [] : ['A', 'B']),
339 +
340 + 'Did mount: A',
341 + 'Did mount: B',
342 + ]);
343 expect(root).toMatchRenderedOutput('AB');
344
345 // Swap the position of A and B
@@ -1395,15 +1403,20 @@ describe('ReactLazy', () => {
1403 unstable_isConcurrent: true,
1404 });
1405
1398 - await waitForAll(['Init A', 'Loading...']);
1406 + await waitForAll([
1407 + 'Init A',
1408 + 'Loading...',
1409 +
1410 + ...(gate('enableSiblingPrerendering') ? ['Init B'] : []),
1411 + ]);
1412 expect(root).not.toMatchRenderedOutput('AB');
1413
1414 await act(() => resolveFakeImport(ChildA));
1415 assertLog([
1416 'A',
1404 - 'Init B',
1417
1406 - ...(gate('enableSiblingPrerendering') ? ['A'] : []),
1418 + // When enableSiblingPrerendering is on, B was already initialized.
1419 + ...(gate('enableSiblingPrerendering') ? ['A'] : ['Init B']),
1420 ]);
1421
1422 await act(() => resolveFakeImport(ChildB));
packages/react-reconciler/src/__tests__/ReactSiblingPrerendering-test.js new
+472
@@ -0,0 +1,472 @@
1 +let React;
2 +let ReactNoop;
3 +let Scheduler;
4 +let act;
5 +let assertLog;
6 +let waitFor;
7 +let waitForPaint;
8 +let waitForAll;
9 +let textCache;
10 +let startTransition;
11 +let Suspense;
12 +let Activity;
13 +
14 +describe('ReactSiblingPrerendering', () => {
15 + beforeEach(() => {
16 + jest.resetModules();
17 +
18 + React = require('react');
19 + ReactNoop = require('react-noop-renderer');
20 + Scheduler = require('scheduler');
21 + act = require('internal-test-utils').act;
22 + assertLog = require('internal-test-utils').assertLog;
23 + waitFor = require('internal-test-utils').waitFor;
24 + waitForPaint = require('internal-test-utils').waitForPaint;
25 + waitForAll = require('internal-test-utils').waitForAll;
26 + startTransition = React.startTransition;
27 + Suspense = React.Suspense;
28 + Activity = React.unstable_Activity;
29 +
30 + textCache = new Map();
31 + });
32 +
33 + function resolveText(text) {
34 + const record = textCache.get(text);
35 + if (record === undefined) {
36 + const newRecord = {
37 + status: 'resolved',
38 + value: text,
39 + };
40 + textCache.set(text, newRecord);
41 + } else if (record.status === 'pending') {
42 + const thenable = record.value;
43 + record.status = 'resolved';
44 + record.value = text;
45 + thenable.pings.forEach(t => t());
46 + }
47 + }
48 +
49 + function readText(text) {
50 + const record = textCache.get(text);
51 + if (record !== undefined) {
52 + switch (record.status) {
53 + case 'pending':
54 + Scheduler.log(`Suspend! [${text}]`);
55 + throw record.value;
56 + case 'rejected':
57 + throw record.value;
58 + case 'resolved':
59 + return record.value;
60 + }
61 + } else {
62 + Scheduler.log(`Suspend! [${text}]`);
63 + const thenable = {
64 + pings: [],
65 + then(resolve) {
66 + if (newRecord.status === 'pending') {
67 + thenable.pings.push(resolve);
68 + } else {
69 + Promise.resolve().then(() => resolve(newRecord.value));
70 + }
71 + },
72 + };
73 +
74 + const newRecord = {
75 + status: 'pending',
76 + value: thenable,
77 + };
78 + textCache.set(text, newRecord);
79 +
80 + throw thenable;
81 + }
82 + }
83 +
84 + // function getText(text) {
85 + // const record = textCache.get(text);
86 + // if (record === undefined) {
87 + // const thenable = {
88 + // pings: [],
89 + // then(resolve) {
90 + // if (newRecord.status === 'pending') {
91 + // thenable.pings.push(resolve);
92 + // } else {
93 + // Promise.resolve().then(() => resolve(newRecord.value));
94 + // }
95 + // },
96 + // };
97 + // const newRecord = {
98 + // status: 'pending',
99 + // value: thenable,
100 + // };
101 + // textCache.set(text, newRecord);
102 + // return thenable;
103 + // } else {
104 + // switch (record.status) {
105 + // case 'pending':
106 + // return record.value;
107 + // case 'rejected':
108 + // return Promise.reject(record.value);
109 + // case 'resolved':
110 + // return Promise.resolve(record.value);
111 + // }
112 + // }
113 + // }
114 +
115 + function Text({text}) {
116 + Scheduler.log(text);
117 + return text;
118 + }
119 +
120 + function AsyncText({text}) {
121 + readText(text);
122 + Scheduler.log(text);
123 + return text;
124 + }
125 +
126 + it("don't prerender siblings when something errors", async () => {
127 + class ErrorBoundary extends React.Component {
128 + state = {error: null};
129 + static getDerivedStateFromError(error) {
130 + return {error};
131 + }
132 + render() {
133 + if (this.state.error) {
134 + return <Text text={this.state.error.message} />;
135 + }
136 + return this.props.children;
137 + }
138 + }
139 +
140 + function Oops() {
141 + throw new Error('Oops!');
142 + }
143 +
144 + function App() {
145 + return (
146 + <>
147 + <div>
148 + <ErrorBoundary>
149 + <Oops />
150 + <AsyncText text="A" />
151 + </ErrorBoundary>
152 + </div>
153 + <div>
154 + <AsyncText text="B" />
155 + <AsyncText text="C" />
156 + </div>
157 + </>
158 + );
159 + }
160 +
161 + const root = ReactNoop.createRoot();
162 + await act(() => startTransition(() => root.render(<App />)));
163 + assertLog([
164 + 'Oops!',
165 +
166 + // A is skipped because we don't prerender siblings when
167 + // something errors.
168 +
169 + 'Suspend! [B]',
170 +
171 + // After B suspends, we're still able to prerender C without starting
172 + // over because there's no fallback, so the root is blocked from
173 + // committing anyway.
174 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [C]'] : []),
175 + ]);
176 + });
177 +
178 + // @gate enableActivity
179 + it("don't skip siblings when rendering inside a hidden tree", async () => {
180 + function App() {
181 + return (
182 + <>
183 + <Text text="A" />
184 + <Activity mode="hidden">
185 + <Suspense fallback={<Text text="Loading..." />}>
186 + <AsyncText text="B" />
187 + <AsyncText text="C" />
188 + </Suspense>
189 + </Activity>
190 + </>
191 + );
192 + }
193 +
194 + const root = ReactNoop.createRoot();
195 + await act(async () => {
196 + startTransition(async () => root.render(<App />));
197 +
198 + // The first render includes only the visible part of the tree. The
199 + // hidden content is deferred until later.
200 + await waitForPaint(['A']);
201 + expect(root).toMatchRenderedOutput('A');
202 +
203 + // The second render is a prerender of the hidden content.
204 + await waitForPaint([
205 + 'Suspend! [B]',
206 +
207 + // If B and C were visible, C would not have been attempted
208 + // during this pass, because it would prevented the fallback
209 + // from showing.
210 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [C]'] : []),
211 +
212 + 'Loading...',
213 + ]);
214 + expect(root).toMatchRenderedOutput('A');
215 + });
216 + });
217 +
218 + it('start prerendering retries right after the fallback commits', async () => {
219 + function App() {
220 + return (
221 + <Suspense fallback={<Text text="Loading..." />}>
222 + <AsyncText text="A" />
223 + <AsyncText text="B" />
224 + </Suspense>
225 + );
226 + }
227 +
228 + const root = ReactNoop.createRoot();
229 + await act(async () => {
230 + startTransition(() => root.render(<App />));
231 +
232 + // On the first attempt, A suspends. Unwind and show a fallback, without
233 + // attempting B.
234 + await waitForPaint(['Suspend! [A]', 'Loading...']);
235 + expect(root).toMatchRenderedOutput('Loading...');
236 +
237 + // Immediately after the fallback commits, retry the boundary again. This
238 + // time we include B, since we're not blocking the fallback from showing.
239 + if (gate('enableSiblingPrerendering')) {
240 + await waitForPaint(['Suspend! [A]', 'Suspend! [B]']);
241 + }
242 + });
243 + expect(root).toMatchRenderedOutput('Loading...');
244 + });
245 +
246 + it('switch back to normal rendering mode if a ping occurs during prerendering', async () => {
247 + function App() {
248 + return (
249 + <div>
250 + <Suspense fallback={<Text text="Loading outer..." />}>
251 + <div>
252 + <Text text="A" />
253 + <AsyncText text="B" />
254 + </div>
255 + <div>
256 + <Suspense fallback={<Text text="Loading inner..." />}>
257 + <AsyncText text="C" />
258 + <AsyncText text="D" />
259 + </Suspense>
260 + </div>
261 + </Suspense>
262 + </div>
263 + );
264 + }
265 +
266 + const root = ReactNoop.createRoot();
267 + await act(async () => {
268 + startTransition(() => root.render(<App />));
269 +
270 + // On the first attempt, B suspends. Unwind and show a fallback, without
271 + // attempting the siblings.
272 + await waitForPaint(['A', 'Suspend! [B]', 'Loading outer...']);
273 + expect(root).toMatchRenderedOutput(<div>Loading outer...</div>);
274 +
275 + // Now that the fallback is visible, we can prerender the siblings. Start
276 + // prerendering, then yield to simulate an interleaved event.
277 + if (gate('enableSiblingPrerendering')) {
278 + await waitFor(['A']);
279 + } else {
280 + await waitForAll([]);
281 + }
282 +
283 + // To avoid the Suspense throttling mechanism, let's pretend there's been
284 + // more than a Just Noticeable Difference since we rendered the
285 + // outer fallback.
286 + Scheduler.unstable_advanceTime(500);
287 +
288 + // During the render phase, but before we get to B again, resolve its
289 + // promise. We should re-enter normal rendering mode, but we also
290 + // shouldn't unwind and lose our work-in-progress.
291 + await resolveText('B');
292 + await waitForPaint([
293 + // When sibling prerendering is not enabled, we weren't already rendering
294 + // when the data for B came in, so A doesn't get rendered until now.
295 + ...(gate('enableSiblingPrerendering') ? [] : ['A']),
296 +
297 + 'B',
298 + 'Suspend! [C]',
299 +
300 + // If we were still in prerendering mode, then we would have attempted
301 + // to render D here. But since we received new data, we will skip the
302 + // remaining siblings to unblock the inner fallback.
303 + 'Loading inner...',
304 + ]);
305 +
306 + expect(root).toMatchRenderedOutput(
307 + <div>
308 + <div>AB</div>
309 + <div>Loading inner...</div>
310 + </div>,
311 + );
312 + });
313 +
314 + // Now that the inner fallback is showing, we can prerender the rest of
315 + // the tree.
316 + assertLog(
317 + gate('enableSiblingPrerendering')
318 + ? [
319 + // NOTE: C renders twice instead of once because when B resolved, it
320 + // was treated like a retry update, not just a ping. So first it
321 + // regular renders, then it prerenders. TODO: We should be able to
322 + // optimize this by detecting inside the retry listener that the
323 + // outer boundary is no longer suspended, and therefore doesn't need
324 + // to be updated.
325 + 'Suspend! [C]',
326 +
327 + // Now we're in prerender mode, so D is incuded in this attempt.
328 + 'Suspend! [C]',
329 + 'Suspend! [D]',
330 + ]
331 + : [],
332 + );
333 + expect(root).toMatchRenderedOutput(
334 + <div>
335 + <div>AB</div>
336 + <div>Loading inner...</div>
337 + </div>,
338 + );
339 + });
340 +
341 + it("don't throw out completed work in order to prerender", async () => {
342 + function App() {
343 + return (
344 + <div>
345 + <Suspense fallback={<Text text="Loading outer..." />}>
346 + <div>
347 + <AsyncText text="A" />
348 + </div>
349 + <div>
350 + <Suspense fallback={<Text text="Loading inner..." />}>
351 + <AsyncText text="B" />
352 + </Suspense>
353 + </div>
354 + </Suspense>
355 + </div>
356 + );
357 + }
358 +
359 + const root = ReactNoop.createRoot();
360 + await act(async () => {
361 + startTransition(() => root.render(<App />));
362 +
363 + await waitForPaint(['Suspend! [A]', 'Loading outer...']);
364 + expect(root).toMatchRenderedOutput(<div>Loading outer...</div>);
365 +
366 + // Before the prerendering of the inner boundary starts, the data for A
367 + // resolves, so we try rendering that again.
368 + await resolveText('A');
369 + // This produces a new tree that we can show. However, the commit phase
370 + // is throttled because it's been less than a Just Noticeable Difference
371 + // since the outer fallback was committed.
372 + //
373 + // In the meantime, we could choose to start prerendering B, but instead
374 + // we wait for a JND to elapse and the commit to finish — it's not
375 + // worth discarding the work we've already done.
376 + await waitForAll(['A', 'Suspend! [B]', 'Loading inner...']);
377 + expect(root).toMatchRenderedOutput(<div>Loading outer...</div>);
378 +
379 + // Fire the timer to commit the outer fallback.
380 + jest.runAllTimers();
381 + expect(root).toMatchRenderedOutput(
382 + <div>
383 + <div>A</div>
384 + <div>Loading inner...</div>
385 + </div>,
386 + );
387 + });
388 + // Once the outer fallback is committed, we can start prerendering B.
389 + assertLog(gate('enableSiblingPrerendering') ? ['Suspend! [B]'] : []);
390 + });
391 +
392 + it(
393 + "don't skip siblings during the retry if there was a ping since the " +
394 + 'first attempt',
395 + async () => {
396 + function App() {
397 + return (
398 + <>
399 + <div>
400 + <Suspense fallback={<Text text="Loading outer..." />}>
401 + <div>
402 + <AsyncText text="A" />
403 + </div>
404 + <div>
405 + <Suspense fallback={<Text text="Loading inner..." />}>
406 + <AsyncText text="B" />
407 + <AsyncText text="C" />
408 + </Suspense>
409 + </div>
410 + </Suspense>
411 + </div>
412 + <div>
413 + <Text text="D" />
414 + </div>
415 + </>
416 + );
417 + }
418 +
419 + const root = ReactNoop.createRoot();
420 + await act(async () => {
421 + startTransition(() => root.render(<App />));
422 +
423 + // On the first attempt, A suspends. Unwind and show a fallback, without
424 + // attempting B or C.
425 + await waitFor([
426 + 'Suspend! [A]',
427 + 'Loading outer...',
428 +
429 + // Yield to simulate an interleaved event
430 + ]);
431 +
432 + // Ping the promise for A before the render phase has finished, as might
433 + // happen in an interleaved network event
434 + await resolveText('A');
435 +
436 + // Now continue rendering the rest of the tree.
437 + await waitForPaint(['D']);
438 + expect(root).toMatchRenderedOutput(
439 + <>
440 + <div>Loading outer...</div>
441 + <div>D</div>
442 + </>,
443 + );
444 +
445 + // Immediately after the fallback commits, retry the boundary again.
446 + // Because the promise for A resolved, this is a normal render, _not_
447 + // a prerender. So when we proceed to B, and B suspends, we unwind again
448 + // without attempting C. The practical benefit of this is that we don't
449 + // block the inner Suspense fallback from appearing.
450 + await waitForPaint(['A', 'Suspend! [B]', 'Loading inner...']);
451 + // (Since this is a retry, the commit phase is throttled by a timer.)
452 + jest.runAllTimers();
453 + // The inner fallback is now visible.
454 + expect(root).toMatchRenderedOutput(
455 + <>
456 + <div>
457 + <div>A</div>
458 + <div>Loading inner...</div>
459 + </div>
460 + <div>D</div>
461 + </>,
462 + );
463 +
464 + // Now we can proceed to prerendering C.
465 + if (gate('enableSiblingPrerendering')) {
466 + await waitForPaint(['Suspend! [B]', 'Suspend! [C]']);
467 + }
468 + });
469 + assertLog([]);
470 + },
471 + );
472 +});
packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
+97 -42
@@ -169,13 +169,23 @@ describe('ReactSuspense', () => {
169 'Loading A...',
170 'Suspend! [B]',
171 'Loading B...',
172 +
173 + ...(gate('enableSiblingPrerendering')
174 + ? ['Suspend! [A]', 'Suspend! [B]']
175 + : []),
176 ]);
177 expect(container.innerHTML).toEqual('Loading A...Loading B...');
178
179 // Resolve first Suspense's promise and switch back to the normal view. The
180 // second Suspense should still show the placeholder
181 await act(() => resolveText('A'));
178 - assertLog(['A']);
182 + assertLog([
183 + 'A',
184 +
185 + ...(gate('enableSiblingPrerendering')
186 + ? ['Suspend! [B]', 'Suspend! [B]']
187 + : []),
188 + ]);
189 expect(container.textContent).toEqual('ALoading B...');
190
191 // Resolve the second Suspense's promise resolves and switche back to the
@@ -274,19 +284,19 @@ describe('ReactSuspense', () => {
284 root.render(<Foo />);
285 });
286
277 - assertLog(['Foo', 'Suspend! [A]', 'Loading...']);
278 - expect(container.textContent).toEqual('Loading...');
279 -
280 - await resolveText('A');
281 - await waitForAll([
282 - 'A',
283 - 'Suspend! [B]',
284 - 'Loading more...',
287 + assertLog([
288 + 'Foo',
289 + 'Suspend! [A]',
290 + 'Loading...',
291
292 ...(gate('enableSiblingPrerendering')
287 - ? ['A', 'Suspend! [B]', 'Loading more...']
293 + ? ['Suspend! [A]', 'Suspend! [B]', 'Loading more...']
294 : []),
295 ]);
296 + expect(container.textContent).toEqual('Loading...');
297 +
298 + await resolveText('A');
299 + await waitForAll(['A', 'Suspend! [B]', 'Loading more...']);
300
301 // By this point, we have enough info to show "A" and "Loading more..."
302 // However, we've just shown the outer fallback. So we'll delay
@@ -327,7 +337,15 @@ describe('ReactSuspense', () => {
337 // Render an empty shell
338 const root = ReactDOMClient.createRoot(container);
339 root.render(<Foo />);
330 - await waitForAll(['Foo', 'Suspend! [A]', 'Loading...']);
340 + await waitForAll([
341 + 'Foo',
342 + 'Suspend! [A]',
343 + 'Loading...',
344 +
345 + ...(gate('enableSiblingPrerendering')
346 + ? ['Suspend! [A]', 'Suspend! [B]', 'Loading more...']
347 + : []),
348 + ]);
349 expect(container.textContent).toEqual('Loading...');
350
351 // Now resolve A
@@ -338,14 +356,7 @@ describe('ReactSuspense', () => {
356 // B starts loading. Parent boundary is in throttle.
357 // Still shows parent loading under throttle
358 jest.advanceTimersByTime(10);
341 - await waitForAll([
342 - 'Suspend! [B]',
343 - 'Loading more...',
344 -
345 - ...(gate('enableSiblingPrerendering')
346 - ? ['A', 'Suspend! [B]', 'Loading more...']
347 - : []),
348 - ]);
359 + await waitForAll(['Suspend! [B]', 'Loading more...']);
360 expect(container.textContent).toEqual('Loading...');
361
362 // !! B could have finished before the throttle, but we show a fallback.
@@ -375,19 +386,19 @@ describe('ReactSuspense', () => {
386 await act(() => {
387 root.render(<Foo />);
388 });
378 - assertLog(['Foo', 'Suspend! [A]', 'Loading...']);
379 - expect(container.textContent).toEqual('Loading...');
380 -
381 - await resolveText('A');
382 - await waitForAll([
383 - 'A',
384 - 'Suspend! [B]',
385 - 'Loading more...',
389 + assertLog([
390 + 'Foo',
391 + 'Suspend! [A]',
392 + 'Loading...',
393
394 ...(gate('enableSiblingPrerendering')
388 - ? ['A', 'Suspend! [B]', 'Loading more...']
395 + ? ['Suspend! [A]', 'Suspend! [B]', 'Loading more...']
396 : []),
397 ]);
398 + expect(container.textContent).toEqual('Loading...');
399 +
400 + await resolveText('A');
401 + await waitForAll(['A', 'Suspend! [B]', 'Loading more...']);
402
403 // By this point, we have enough info to show "A" and "Loading more..."
404 // However, we've just shown the outer fallback. So we'll delay
@@ -468,14 +479,24 @@ describe('ReactSuspense', () => {
479 await act(() => {
480 root.render(<App />);
481 });
471 - assertLog(['Suspend! [default]', 'Loading...']);
482 + assertLog([
483 + 'Suspend! [default]',
484 + 'Loading...',
485 +
486 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [default]'] : []),
487 + ]);
488
489 await act(() => resolveText('default'));
490 assertLog(['default']);
491 expect(container.textContent).toEqual('default');
492
493 await act(() => setValue('new value'));
478 - assertLog(['Suspend! [new value]', 'Loading...']);
494 + assertLog([
495 + 'Suspend! [new value]',
496 + 'Loading...',
497 +
498 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [new value]'] : []),
499 + ]);
500
501 await act(() => resolveText('new value'));
502 assertLog(['new value']);
@@ -515,14 +536,24 @@ describe('ReactSuspense', () => {
536 await act(() => {
537 root.render(<App />);
538 });
518 - assertLog(['Suspend! [default]', 'Loading...']);
539 + assertLog([
540 + 'Suspend! [default]',
541 + 'Loading...',
542 +
543 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [default]'] : []),
544 + ]);
545
546 await act(() => resolveText('default'));
547 assertLog(['default']);
548 expect(container.textContent).toEqual('default');
549
550 await act(() => setValue('new value'));
525 - assertLog(['Suspend! [new value]', 'Loading...']);
551 + assertLog([
552 + 'Suspend! [new value]',
553 + 'Loading...',
554 +
555 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [new value]'] : []),
556 + ]);
557
558 await act(() => resolveText('new value'));
559 assertLog(['new value']);
@@ -559,14 +590,24 @@ describe('ReactSuspense', () => {
590 </App>,
591 );
592 });
562 - assertLog(['Suspend! [default]', 'Loading...']);
593 + assertLog([
594 + 'Suspend! [default]',
595 + 'Loading...',
596 +
597 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [default]'] : []),
598 + ]);
599
600 await act(() => resolveText('default'));
601 assertLog(['default']);
602 expect(container.textContent).toEqual('default');
603
604 await act(() => setValue('new value'));
569 - assertLog(['Suspend! [new value]', 'Loading...']);
605 + assertLog([
606 + 'Suspend! [new value]',
607 + 'Loading...',
608 +
609 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [new value]'] : []),
610 + ]);
611
612 await act(() => resolveText('new value'));
613 assertLog(['new value']);
@@ -603,14 +644,24 @@ describe('ReactSuspense', () => {
644 </App>,
645 );
646 });
606 - assertLog(['Suspend! [default]', 'Loading...']);
647 + assertLog([
648 + 'Suspend! [default]',
649 + 'Loading...',
650 +
651 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [default]'] : []),
652 + ]);
653
654 await act(() => resolveText('default'));
655 assertLog(['default']);
656 expect(container.textContent).toEqual('default');
657
658 await act(() => setValue('new value'));
613 - assertLog(['Suspend! [new value]', 'Loading...']);
659 + assertLog([
660 + 'Suspend! [new value]',
661 + 'Loading...',
662 +
663 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [new value]'] : []),
664 + ]);
665
666 await act(() => resolveText('new value'));
667 assertLog(['new value']);
@@ -657,6 +708,10 @@ describe('ReactSuspense', () => {
708 'Suspend! [Child 2]',
709 'Loading...',
710 'destroy layout',
711 +
712 + ...(gate('enableSiblingPrerendering')
713 + ? ['Child 1', 'Suspend! [Child 2]']
714 + : []),
715 ]);
716
717 await act(() => resolveText('Child 2'));
@@ -679,16 +734,16 @@ describe('ReactSuspense', () => {
734 root.render(<App />);
735 });
736
682 - assertLog(['Suspend! [Child 1]', 'Loading...']);
683 - await resolveText('Child 1');
684 - await waitForAll([
685 - 'Child 1',
686 - 'Suspend! [Child 2]',
737 + assertLog([
738 + 'Suspend! [Child 1]',
739 + 'Loading...',
740
741 ...(gate('enableSiblingPrerendering')
689 - ? ['Child 1', 'Suspend! [Child 2]']
742 + ? ['Suspend! [Child 1]', 'Suspend! [Child 2]']
743 : []),
744 ]);
745 + await resolveText('Child 1');
746 + await waitForAll(['Child 1', 'Suspend! [Child 2]']);
747
748 jest.advanceTimersByTime(6000);
749
packages/react-reconciler/src/__tests__/ReactSuspenseCallback-test.js
+10 -2
@@ -136,7 +136,11 @@ describe('ReactSuspense', () => {
136 ReactNoop.render(element);
137 await waitForAll([]);
138 expect(ReactNoop).toMatchRenderedOutput('Waiting Tier 1');
139 - expect(ops).toEqual([new Set([promise2])]);
139 + expect(ops).toEqual([
140 + new Set([promise2]),
141 +
142 + ...(gate('enableSiblingPrerendering') ? new Set([promise2]) : []),
143 + ]);
144 ops = [];
145
146 await act(() => resolve2());
@@ -224,7 +228,11 @@ describe('ReactSuspense', () => {
228 await act(() => resolve1());
229 expect(ReactNoop).toMatchRenderedOutput('Waiting Tier 2Done');
230 expect(ops1).toEqual([]);
227 - expect(ops2).toEqual([new Set([promise2])]);
231 + expect(ops2).toEqual([
232 + new Set([promise2]),
233 +
234 + ...(gate('enableSiblingPrerendering') ? new Set([promise2]) : []),
235 + ]);
236 ops1 = [];
237 ops2 = [];
238
packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js
+193 -15
@@ -274,6 +274,14 @@ describe('ReactSuspenseEffectsSemantics', () => {
274 'Text:Fallback create passive',
275 'Text:Outside create passive',
276 'App create passive',
277 +
278 + ...(gate('enableSiblingPrerendering')
279 + ? [
280 + 'Text:Inside:Before render',
281 + 'Suspend:Async',
282 + 'ClassText:Inside:After render',
283 + ]
284 + : []),
285 ]);
286 expect(ReactNoop).toMatchRenderedOutput(
287 <>
@@ -646,7 +654,17 @@ describe('ReactSuspenseEffectsSemantics', () => {
654 'Text:Inside:After destroy layout',
655 'Text:Fallback create layout',
656 ]);
649 - await waitForAll(['Text:Fallback create passive']);
657 + await waitForAll([
658 + 'Text:Fallback create passive',
659 +
660 + ...(gate('enableSiblingPrerendering')
661 + ? [
662 + 'Text:Inside:Before render',
663 + 'Suspend:Async',
664 + 'Text:Inside:After render',
665 + ]
666 + : []),
667 + ]);
668 expect(ReactNoop).toMatchRenderedOutput(
669 <>
670 <span prop="Inside:Before" hidden={true} />
@@ -797,6 +815,13 @@ describe('ReactSuspenseEffectsSemantics', () => {
815 </>,
816 );
817 });
818 + if (gate('enableSiblingPrerendering')) {
819 + assertLog([
820 + 'ClassText:Inside:Before render',
821 + 'Suspend:Async',
822 + 'ClassText:Inside:After render',
823 + ]);
824 + }
825
826 // Resolving the suspended resource should re-create inner layout effects.
827 await act(async () => {
@@ -896,7 +921,13 @@ describe('ReactSuspenseEffectsSemantics', () => {
921 'Text:Inner destroy layout',
922 'Text:Fallback create layout',
923 ]);
899 - await waitForAll(['Text:Fallback create passive']);
924 + await waitForAll([
925 + 'Text:Fallback create passive',
926 +
927 + ...(gate('enableSiblingPrerendering')
928 + ? ['Suspend:Async', 'Text:Outer render', 'Text:Inner render']
929 + : []),
930 + ]);
931 expect(ReactNoop).toMatchRenderedOutput(
932 <>
933 <span hidden={true} prop="Outer">
@@ -1011,7 +1042,13 @@ describe('ReactSuspenseEffectsSemantics', () => {
1042 'Text:MemoizedInner destroy layout',
1043 'Text:Fallback create layout',
1044 ]);
1014 - await waitForAll(['Text:Fallback create passive']);
1045 + await waitForAll([
1046 + 'Text:Fallback create passive',
1047 +
1048 + ...(gate('enableSiblingPrerendering')
1049 + ? ['Suspend:Async', 'Text:Outer render']
1050 + : []),
1051 + ]);
1052 expect(ReactNoop).toMatchRenderedOutput(
1053 <>
1054 <span hidden={true} prop="Outer">
@@ -1108,6 +1145,10 @@ describe('ReactSuspenseEffectsSemantics', () => {
1145 'Text:Inner destroy layout',
1146 'Text:InnerFallback create layout',
1147 'Text:InnerFallback create passive',
1148 +
1149 + ...(gate('enableSiblingPrerendering')
1150 + ? ['Text:Inner render', 'Suspend:InnerAsync_1']
1151 + : []),
1152 ]);
1153 expect(ReactNoop).toMatchRenderedOutput(
1154 <>
@@ -1136,6 +1177,16 @@ describe('ReactSuspenseEffectsSemantics', () => {
1177 'Text:InnerFallback destroy layout',
1178 'Text:OuterFallback create layout',
1179 'Text:OuterFallback create passive',
1180 +
1181 + ...(gate('enableSiblingPrerendering')
1182 + ? [
1183 + 'Text:Outer render',
1184 + 'Suspend:OuterAsync_1',
1185 + 'Text:Inner render',
1186 + 'Suspend:InnerAsync_1',
1187 + 'Text:InnerFallback render',
1188 + ]
1189 + : []),
1190 ]);
1191 expect(ReactNoop).toMatchRenderedOutput(
1192 <>
@@ -1186,6 +1237,16 @@ describe('ReactSuspenseEffectsSemantics', () => {
1237 'Text:Outer render',
1238 'Suspend:OuterAsync_1',
1239 'Text:OuterFallback render',
1240 +
1241 + ...(gate('enableSiblingPrerendering')
1242 + ? [
1243 + 'Text:Outer render',
1244 + 'Suspend:OuterAsync_1',
1245 + 'Text:Inner render',
1246 + 'Suspend:InnerAsync_2',
1247 + 'Text:InnerFallback render',
1248 + ]
1249 + : []),
1250 ]);
1251 expect(ReactNoop).toMatchRenderedOutput(
1252 <>
@@ -1207,22 +1268,16 @@ describe('ReactSuspenseEffectsSemantics', () => {
1268 'Suspend:InnerAsync_2',
1269 'Text:InnerFallback render',
1270
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 -
1271 'Text:OuterFallback destroy layout',
1272 'Text:Outer create layout',
1273 'AsyncText:OuterAsync_1 create layout',
1274 'Text:InnerFallback create layout',
1275 'Text:OuterFallback destroy passive',
1276 'AsyncText:OuterAsync_1 create passive',
1277 +
1278 + ...(gate('enableSiblingPrerendering')
1279 + ? ['Text:Inner render', 'Suspend:InnerAsync_2']
1280 + : []),
1281 ]);
1282 expect(ReactNoop).toMatchRenderedOutput(
1283 <>
@@ -1274,6 +1329,15 @@ describe('ReactSuspenseEffectsSemantics', () => {
1329 'AsyncText:InnerAsync_2 destroy layout',
1330 'Text:OuterFallback create layout',
1331 'Text:OuterFallback create passive',
1332 +
1333 + ...(gate('enableSiblingPrerendering')
1334 + ? [
1335 + 'Text:Outer render',
1336 + 'Suspend:OuterAsync_2',
1337 + 'Text:Inner render',
1338 + 'AsyncText:InnerAsync_2 render',
1339 + ]
1340 + : []),
1341 ]);
1342 expect(ReactNoop).toMatchRenderedOutput(
1343 <>
@@ -1359,6 +1423,10 @@ describe('ReactSuspenseEffectsSemantics', () => {
1423 'Text:Inner destroy layout',
1424 'Text:InnerFallback create layout',
1425 'Text:InnerFallback create passive',
1426 +
1427 + ...(gate('enableSiblingPrerendering')
1428 + ? ['Text:Inner render', 'Suspend:InnerAsync_1']
1429 + : []),
1430 ]);
1431 expect(ReactNoop).toMatchRenderedOutput(
1432 <>
@@ -1386,6 +1454,16 @@ describe('ReactSuspenseEffectsSemantics', () => {
1454 'Text:InnerFallback destroy layout',
1455 'Text:OuterFallback create layout',
1456 'Text:OuterFallback create passive',
1457 +
1458 + ...(gate('enableSiblingPrerendering')
1459 + ? [
1460 + 'Text:Outer render',
1461 + 'Suspend:OuterAsync_1',
1462 + 'Text:Inner render',
1463 + 'Suspend:InnerAsync_1',
1464 + 'Text:InnerFallback render',
1465 + ]
1466 + : []),
1467 ]);
1468 expect(ReactNoop).toMatchRenderedOutput(
1469 <>
@@ -1486,6 +1564,10 @@ describe('ReactSuspenseEffectsSemantics', () => {
1564 await waitForAll([
1565 'Text:Fallback:Inside create passive',
1566 'Text:Fallback:Outside create passive',
1567 +
1568 + ...(gate('enableSiblingPrerendering')
1569 + ? ['Text:Inside render', 'Suspend:OutsideAsync']
1570 + : []),
1571 ]);
1572 expect(ReactNoop).toMatchRenderedOutput(
1573 <>
@@ -1516,7 +1598,18 @@ describe('ReactSuspenseEffectsSemantics', () => {
1598 'Text:Fallback:Inside destroy layout',
1599 'Text:Fallback:Fallback create layout',
1600 ]);
1519 - await waitForAll(['Text:Fallback:Fallback create passive']);
1601 + await waitForAll([
1602 + 'Text:Fallback:Fallback create passive',
1603 +
1604 + ...(gate('enableSiblingPrerendering')
1605 + ? [
1606 + 'Text:Inside render',
1607 + 'Suspend:OutsideAsync',
1608 + 'Text:Fallback:Inside render',
1609 + 'Suspend:FallbackAsync',
1610 + ]
1611 + : []),
1612 + ]);
1613 expect(ReactNoop).toMatchRenderedOutput(
1614 <>
1615 <span prop="Inside" hidden={true} />
@@ -1618,6 +1711,15 @@ describe('ReactSuspenseEffectsSemantics', () => {
1711 'Text:Fallback:Outside create layout',
1712 'Text:Fallback:Fallback create passive',
1713 'Text:Fallback:Outside create passive',
1714 +
1715 + ...(gate('enableSiblingPrerendering')
1716 + ? [
1717 + 'Text:Inside render',
1718 + 'Suspend:OutsideAsync',
1719 + 'Text:Fallback:Inside render',
1720 + 'Suspend:FallbackAsync',
1721 + ]
1722 + : []),
1723 ]);
1724 expect(ReactNoop).toMatchRenderedOutput(
1725 <>
@@ -1728,7 +1830,11 @@ describe('ReactSuspenseEffectsSemantics', () => {
1830 'Text:Inside destroy layout',
1831 'Text:Fallback create layout',
1832 ]);
1731 - await waitForAll(['Text:Fallback create passive']);
1833 + await waitForAll([
1834 + 'Text:Fallback create passive',
1835 +
1836 + ...(gate('enableSiblingPrerendering') ? ['Suspend:Suspend'] : []),
1837 + ]);
1838 expect(ReactNoop).toMatchRenderedOutput(
1839 <>
1840 <span prop="Inside" hidden={true} />
@@ -1845,6 +1951,10 @@ describe('ReactSuspenseEffectsSemantics', () => {
1951 'Text:Inside destroy layout',
1952 'Text:Fallback create layout',
1953 'Text:Fallback create passive',
1954 +
1955 + ...(gate('enableSiblingPrerendering')
1956 + ? ['Suspend:Async', 'ThrowsInDidMount render', 'Text:Inside render']
1957 + : []),
1958 ]);
1959 expect(ReactNoop).toMatchRenderedOutput(
1960 <>
@@ -2088,6 +2198,14 @@ describe('ReactSuspenseEffectsSemantics', () => {
2198 'Text:Inside destroy layout',
2199 'Text:Fallback create layout',
2200 'Text:Fallback create passive',
2201 +
2202 + ...(gate('enableSiblingPrerendering')
2203 + ? [
2204 + 'Suspend:Async',
2205 + 'ThrowsInLayoutEffect render',
2206 + 'Text:Inside render',
2207 + ]
2208 + : []),
2209 ]);
2210 expect(ReactNoop).toMatchRenderedOutput(
2211 <>
@@ -2320,6 +2438,15 @@ describe('ReactSuspenseEffectsSemantics', () => {
2438 );
2439 });
2440
2441 + if (gate('enableSiblingPrerendering')) {
2442 + assertLog([
2443 + 'Text:Function render',
2444 + 'Suspend:Async_1',
2445 + 'Suspend:Async_2',
2446 + 'ClassText:Class render',
2447 + ]);
2448 + }
2449 +
2450 // Resolving the suspended resource should re-create inner layout effects.
2451 await act(async () => {
2452 await resolveText('Async_1');
@@ -2469,6 +2596,14 @@ describe('ReactSuspenseEffectsSemantics', () => {
2596 </>,
2597 );
2598 });
2599 + if (gate('enableSiblingPrerendering')) {
2600 + assertLog([
2601 + 'Text:Function render',
2602 + 'Suspender "A" render',
2603 + 'Suspend:A',
2604 + 'ClassText:Class render',
2605 + ]);
2606 + }
2607
2608 // Resolving the suspended resource should re-create inner layout effects.
2609 textToRead = 'B';
@@ -2719,6 +2854,15 @@ describe('ReactSuspenseEffectsSemantics', () => {
2854 'RefCheckerInner:refCallback destroy layout ref? false',
2855 'Text:Fallback create layout',
2856 'Text:Fallback create passive',
2857 +
2858 + ...(gate('enableSiblingPrerendering')
2859 + ? [
2860 + 'Suspend:Async',
2861 + 'RefCheckerOuter render',
2862 + 'RefCheckerInner:refObject render',
2863 + 'RefCheckerInner:refCallback render',
2864 + ]
2865 + : []),
2866 ]);
2867 expect(ReactNoop).toMatchRenderedOutput(
2868 <>
@@ -2820,6 +2964,17 @@ describe('ReactSuspenseEffectsSemantics', () => {
2964 'RefCheckerInner:refCallback destroy layout ref? false',
2965 'Text:Fallback create layout',
2966 'Text:Fallback create passive',
2967 +
2968 + ...(gate('enableSiblingPrerendering')
2969 + ? [
2970 + 'Suspend:Async',
2971 + 'RefCheckerOuter render',
2972 + 'ClassComponent:refObject render',
2973 + 'RefCheckerInner:refObject render',
2974 + 'ClassComponent:refCallback render',
2975 + 'RefCheckerInner:refCallback render',
2976 + ]
2977 + : []),
2978 ]);
2979 expect(ReactNoop).toMatchRenderedOutput(<span prop="Fallback" />);
2980
@@ -2917,6 +3072,17 @@ describe('ReactSuspenseEffectsSemantics', () => {
3072 'RefCheckerInner:refCallback destroy layout ref? false',
3073 'Text:Fallback create layout',
3074 'Text:Fallback create passive',
3075 +
3076 + ...(gate('enableSiblingPrerendering')
3077 + ? [
3078 + 'Suspend:Async',
3079 + 'RefCheckerOuter render',
3080 + 'FunctionComponent render',
3081 + 'RefCheckerInner:refObject render',
3082 + 'FunctionComponent render',
3083 + 'RefCheckerInner:refCallback render',
3084 + ]
3085 + : []),
3086 ]);
3087 expect(ReactNoop).toMatchRenderedOutput(<span prop="Fallback" />);
3088
@@ -3016,6 +3182,10 @@ describe('ReactSuspenseEffectsSemantics', () => {
3182 'RefChecker destroy layout ref? true',
3183 'Text:Fallback create layout',
3184 'Text:Fallback create passive',
3185 +
3186 + ...(gate('enableSiblingPrerendering')
3187 + ? ['Suspend:Async', 'RefChecker render']
3188 + : []),
3189 ]);
3190 expect(ReactNoop).toMatchRenderedOutput(<span prop="Fallback" />);
3191
@@ -3130,6 +3300,14 @@ describe('ReactSuspenseEffectsSemantics', () => {
3300 'Text:Inside destroy layout',
3301 'Text:Fallback create layout',
3302 'Text:Fallback create passive',
3303 +
3304 + ...(gate('enableSiblingPrerendering')
3305 + ? [
3306 + 'Suspend:Async',
3307 + 'ThrowsInRefCallback render',
3308 + 'Text:Inside render',
3309 + ]
3310 + : []),
3311 ]);
3312 expect(ReactNoop).toMatchRenderedOutput(
3313 <>
packages/react-reconciler/src/__tests__/ReactSuspenseFallback-test.js
+20 -2
@@ -139,7 +139,12 @@ describe('ReactSuspenseFallback', () => {
139 </Suspense>,
140 );
141
142 - await waitForAll(['Suspend! [A]', 'Loading...']);
142 + await waitForAll([
143 + 'Suspend! [A]',
144 + 'Loading...',
145 +
146 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [A]'] : []),
147 + ]);
148 expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
149 });
150
@@ -154,6 +159,8 @@ describe('ReactSuspenseFallback', () => {
159 await waitForAll([
160 'Suspend! [A]',
161 // null
162 +
163 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [A]'] : []),
164 ]);
165 expect(ReactNoop).toMatchRenderedOutput(null);
166 });
@@ -169,6 +176,8 @@ describe('ReactSuspenseFallback', () => {
176 await waitForAll([
177 'Suspend! [A]',
178 // null
179 +
180 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [A]'] : []),
181 ]);
182 expect(ReactNoop).toMatchRenderedOutput(null);
183 });
@@ -183,7 +192,12 @@ describe('ReactSuspenseFallback', () => {
192 </Suspense>,
193 );
194
186 - await waitForAll(['Suspend! [A]', 'Loading...']);
195 + await waitForAll([
196 + 'Suspend! [A]',
197 + 'Loading...',
198 +
199 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [A]'] : []),
200 + ]);
201 expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
202 });
203
@@ -200,6 +214,8 @@ describe('ReactSuspenseFallback', () => {
214 await waitForAll([
215 'Suspend! [A]',
216 // null
217 +
218 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [A]'] : []),
219 ]);
220 expect(ReactNoop).toMatchRenderedOutput(null);
221 });
@@ -217,6 +233,8 @@ describe('ReactSuspenseFallback', () => {
233 await waitForAll([
234 'Suspend! [A]',
235 // null
236 +
237 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [A]'] : []),
238 ]);
239 expect(ReactNoop).toMatchRenderedOutput(null);
240 });
packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js
+115 -148
@@ -239,6 +239,9 @@ describe('ReactSuspenseList', () => {
239 'Loading B',
240 'Suspend! [C]',
241 'Loading C',
242 + ...(gate('enableSiblingPrerendering')
243 + ? ['Suspend! [B]', 'Suspend! [C]']
244 + : []),
245 ]);
246
247 expect(ReactNoop).toMatchRenderedOutput(
@@ -250,7 +253,11 @@ describe('ReactSuspenseList', () => {
253 );
254
255 await act(() => C.resolve());
253 - assertLog(['C']);
256 + assertLog(
257 + gate('enableSiblingPrerendering')
258 + ? ['Suspend! [B]', 'C', 'Suspend! [B]']
259 + : ['C'],
260 + );
261
262 expect(ReactNoop).toMatchRenderedOutput(
263 <>
@@ -383,13 +390,7 @@ describe('ReactSuspenseList', () => {
390 );
391
392 await act(() => B.resolve());
386 - assertLog([
387 - 'A',
388 - 'B',
389 - 'Suspend! [C]',
390 -
391 - ...(gate('enableSiblingPrerendering') ? ['A', 'B', 'Suspend! [C]'] : []),
392 - ]);
393 + assertLog(['A', 'B', 'Suspend! [C]']);
394
395 expect(ReactNoop).toMatchRenderedOutput(
396 <>
@@ -465,13 +466,7 @@ describe('ReactSuspenseList', () => {
466 );
467
468 await act(() => B.resolve());
468 - assertLog([
469 - 'A',
470 - 'B',
471 - 'Suspend! [C]',
472 -
473 - ...(gate('enableSiblingPrerendering') ? ['A', 'B', 'Suspend! [C]'] : []),
474 - ]);
469 + assertLog(['A', 'B', 'Suspend! [C]']);
470
471 expect(ReactNoop).toMatchRenderedOutput(
472 <>
@@ -742,7 +737,11 @@ describe('ReactSuspenseList', () => {
737
738 ReactNoop.render(<Foo />);
739
745 - await waitForAll(['Suspend! [A]', 'Loading']);
740 + await waitForAll([
741 + 'Suspend! [A]',
742 + 'Loading',
743 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [A]'] : []),
744 + ]);
745
746 expect(ReactNoop).toMatchRenderedOutput(<span>Loading</span>);
747
@@ -779,12 +778,7 @@ describe('ReactSuspenseList', () => {
778 );
779
780 await act(() => B.resolve());
782 - assertLog([
783 - 'B',
784 - 'Suspend! [C]',
785 -
786 - ...(gate('enableSiblingPrerendering') ? ['B', 'Suspend! [C]'] : []),
787 - ]);
781 + assertLog(['B', 'Suspend! [C]']);
782
783 // Even though we could now show B, we're still waiting on C.
784 expect(ReactNoop).toMatchRenderedOutput(
@@ -871,12 +865,7 @@ describe('ReactSuspenseList', () => {
865 expect(ReactNoop).toMatchRenderedOutput(<span>A</span>);
866
867 await act(() => B.resolve());
874 - assertLog([
875 - 'B',
876 - 'Suspend! [C]',
877 -
878 - ...(gate('enableSiblingPrerendering') ? ['B', 'Suspend! [C]'] : []),
879 - ]);
868 + assertLog(['B', 'Suspend! [C]']);
869
870 // Even though we could now show B, we're still waiting on C.
871 expect(ReactNoop).toMatchRenderedOutput(<span>A</span>);
@@ -919,7 +908,14 @@ describe('ReactSuspenseList', () => {
908
909 ReactNoop.render(<Foo />);
910
922 - await waitForAll(['Suspend! [A]', 'Loading A', 'Loading B', 'Loading C']);
911 + await waitForAll([
912 + 'Suspend! [A]',
913 + 'Loading A',
914 + 'Loading B',
915 + 'Loading C',
916 +
917 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [A]'] : []),
918 + ]);
919
920 expect(ReactNoop).toMatchRenderedOutput(
921 <>
@@ -934,7 +930,7 @@ describe('ReactSuspenseList', () => {
930 'A',
931 'Suspend! [B]',
932
937 - ...(gate('enableSiblingPrerendering') ? ['A', 'Suspend! [B]'] : []),
933 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [B]'] : []),
934 ]);
935
936 expect(ReactNoop).toMatchRenderedOutput(
@@ -983,7 +979,14 @@ describe('ReactSuspenseList', () => {
979
980 ReactNoop.render(<Foo />);
981
986 - await waitForAll(['Suspend! [C]', 'Loading C', 'Loading B', 'Loading A']);
982 + await waitForAll([
983 + 'Suspend! [C]',
984 + 'Loading C',
985 + 'Loading B',
986 + 'Loading A',
987 +
988 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [C]'] : []),
989 + ]);
990
991 expect(ReactNoop).toMatchRenderedOutput(
992 <>
@@ -998,7 +1001,7 @@ describe('ReactSuspenseList', () => {
1001 'C',
1002 'Suspend! [B]',
1003
1001 - ...(gate('enableSiblingPrerendering') ? ['C', 'Suspend! [B]'] : []),
1004 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [B]'] : []),
1005 ]);
1006
1007 expect(ReactNoop).toMatchRenderedOutput(
@@ -1104,12 +1107,7 @@ describe('ReactSuspenseList', () => {
1107 );
1108
1109 await act(() => A.resolve());
1107 - assertLog([
1108 - 'A',
1109 - 'Suspend! [C]',
1110 -
1111 - ...(gate('enableSiblingPrerendering') ? ['A', 'Suspend! [C]'] : []),
1112 - ]);
1110 + assertLog(['A', 'Suspend! [C]']);
1111
1112 // Even though we could show A, it is still in a fallback state because
1113 // C is not yet resolved. We need to resolve everything in the head first.
@@ -1130,7 +1128,7 @@ describe('ReactSuspenseList', () => {
1128 'C',
1129 'Suspend! [E]',
1130
1133 - ...(gate('enableSiblingPrerendering') ? ['A', 'C', 'Suspend! [E]'] : []),
1131 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [E]'] : []),
1132 ]);
1133
1134 // We can now resolve the full head.
@@ -1150,7 +1148,7 @@ describe('ReactSuspenseList', () => {
1148 'E',
1149 'Suspend! [F]',
1150
1153 - ...(gate('enableSiblingPrerendering') ? ['E', 'Suspend! [F]'] : []),
1151 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [F]'] : []),
1152 ]);
1153
1154 // In the tail we can resolve one-by-one.
@@ -1292,6 +1290,10 @@ describe('ReactSuspenseList', () => {
1290 'E',
1291 'Suspend! [F]',
1292 'Loading F',
1293 +
1294 + ...(gate('enableSiblingPrerendering')
1295 + ? ['Suspend! [D]', 'Suspend! [F]']
1296 + : []),
1297 ]);
1298
1299 // This will suspend, since the boundaries are avoided. Give them
@@ -1315,12 +1317,7 @@ describe('ReactSuspenseList', () => {
1317
1318 await F.resolve();
1319
1318 - await waitForAll([
1319 - 'Suspend! [D]',
1320 - 'F',
1321 -
1322 - ...(gate('enableSiblingPrerendering') ? ['Suspend! [D]', 'F'] : []),
1323 - ]);
1320 + await waitForAll(['Suspend! [D]', 'F']);
1321
1322 // Even though we could show F, it is still in a fallback state because
1323 // E is not yet resolved. We need to resolve everything in the head first.
@@ -1345,7 +1342,7 @@ describe('ReactSuspenseList', () => {
1342 'F',
1343 'Suspend! [B]',
1344
1348 - ...(gate('enableSiblingPrerendering') ? ['D', 'F', 'Suspend! [B]'] : []),
1345 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [B]'] : []),
1346 ]);
1347
1348 // We can now resolve the full head.
@@ -1367,7 +1364,7 @@ describe('ReactSuspenseList', () => {
1364 'B',
1365 'Suspend! [A]',
1366
1370 - ...(gate('enableSiblingPrerendering') ? ['B', 'Suspend! [A]'] : []),
1367 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [A]'] : []),
1368 ]);
1369
1370 // In the tail we can resolve one-by-one.
@@ -1487,21 +1484,18 @@ describe('ReactSuspenseList', () => {
1484
1485 ReactNoop.render(<Foo />);
1486
1490 - await waitForAll(['Suspend! [A]', 'Loading A']);
1487 + await waitForAll([
1488 + 'Suspend! [A]',
1489 + 'Loading A',
1490 +
1491 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [A]'] : []),
1492 + ]);
1493
1494 expect(ReactNoop).toMatchRenderedOutput(<span>Loading A</span>);
1495
1496 await A.resolve();
1497
1496 - await waitForAll([
1497 - 'A',
1498 - 'Suspend! [B]',
1499 - 'Loading B',
1500 -
1501 - ...(gate('enableSiblingPrerendering')
1502 - ? ['A', 'Suspend! [B]', 'Loading B']
1503 - : []),
1504 - ]);
1498 + await waitForAll(['A', 'Suspend! [B]', 'Loading B']);
1499
1500 // Incremental loading is suspended.
1501 jest.advanceTimersByTime(500);
@@ -1515,15 +1509,7 @@ describe('ReactSuspenseList', () => {
1509
1510 await B.resolve();
1511
1518 - await waitForAll([
1519 - 'B',
1520 - 'Suspend! [C]',
1521 - 'Loading C',
1522 -
1523 - ...(gate('enableSiblingPrerendering')
1524 - ? ['B', 'Suspend! [C]', 'Loading C']
1525 - : []),
1526 - ]);
1512 + await waitForAll(['B', 'Suspend! [C]', 'Loading C']);
1513
1514 // Incremental loading is suspended.
1515 jest.advanceTimersByTime(500);
@@ -1747,12 +1733,7 @@ describe('ReactSuspenseList', () => {
1733
1734 await B.resolve();
1735
1750 - await waitForAll([
1751 - 'B',
1752 - 'Suspend! [C]',
1753 -
1754 - ...(gate('enableSiblingPrerendering') ? ['B', 'Suspend! [C]'] : []),
1755 - ]);
1736 + await waitForAll(['B', 'Suspend! [C]']);
1737
1738 // Incremental loading is suspended.
1739 jest.advanceTimersByTime(500);
@@ -1772,17 +1753,7 @@ describe('ReactSuspenseList', () => {
1753 await C.resolve();
1754 await E.resolve();
1755
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 - ]);
1756 + await waitForAll(['B', 'C', 'E', 'Suspend! [F]', 'Loading F']);
1757
1758 jest.advanceTimersByTime(500);
1759
@@ -1899,12 +1870,7 @@ describe('ReactSuspenseList', () => {
1870
1871 await D.resolve();
1872
1902 - await waitForAll([
1903 - 'D',
1904 - 'Suspend! [E]',
1905 -
1906 - ...(gate('enableSiblingPrerendering') ? ['D', 'Suspend! [E]'] : []),
1907 - ]);
1873 + await waitForAll(['D', 'Suspend! [E]']);
1874
1875 // Incremental loading is suspended.
1876 jest.advanceTimersByTime(500);
@@ -1929,17 +1895,7 @@ describe('ReactSuspenseList', () => {
1895 await D.resolve();
1896 await E.resolve();
1897
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 - ]);
1898 + await waitForAll(['D', 'E', 'B', 'Suspend! [A]', 'Loading A']);
1899
1900 jest.advanceTimersByTime(500);
1901
@@ -2047,6 +2003,8 @@ describe('ReactSuspenseList', () => {
2003 'Suspend! [D]',
2004 'Loading D',
2005 'Loading E',
2006 +
2007 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [B]'] : []),
2008 ]);
2009
2010 // This is suspended due to the update to D causing a loading state.
@@ -2068,12 +2026,7 @@ describe('ReactSuspenseList', () => {
2026
2027 await B.resolve();
2028
2071 - await waitForAll([
2072 - 'B',
2073 - 'Suspend! [C]',
2074 -
2075 - ...(gate('enableSiblingPrerendering') ? ['B', 'Suspend! [C]'] : []),
2076 - ]);
2029 + await waitForAll(['B', 'Suspend! [C]']);
2030
2031 // Incremental loading is suspended.
2032 jest.advanceTimersByTime(500);
@@ -2096,17 +2049,7 @@ describe('ReactSuspenseList', () => {
2049 await D.resolve();
2050 await E.resolve();
2051
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 - ]);
2052 + await waitForAll(['C', 'D', 'E', 'Suspend! [F]', 'Loading F']);
2053
2054 jest.advanceTimersByTime(500);
2055
@@ -2169,15 +2112,7 @@ describe('ReactSuspenseList', () => {
2112
2113 await A.resolve();
2114
2172 - await waitForAll([
2173 - 'A',
2174 - 'Suspend! [B]',
2175 - 'Loading B',
2176 -
2177 - ...(gate('enableSiblingPrerendering')
2178 - ? ['A', 'Suspend! [B]', 'Loading B']
2179 - : []),
2180 - ]);
2115 + await waitForAll(['A', 'Suspend! [B]', 'Loading B']);
2116
2117 // Incremental loading is suspended.
2118 jest.advanceTimersByTime(500);
@@ -2185,15 +2120,7 @@ describe('ReactSuspenseList', () => {
2120 expect(ReactNoop).toMatchRenderedOutput(<span>A</span>);
2121
2122 await act(() => B.resolve());
2188 - assertLog([
2189 - 'B',
2190 - 'Suspend! [C]',
2191 - 'Loading C',
2192 -
2193 - ...(gate('enableSiblingPrerendering')
2194 - ? ['B', 'Suspend! [C]', 'Loading C']
2195 - : []),
2196 - ]);
2123 + assertLog(['B', 'Suspend! [C]', 'Loading C']);
2124
2125 // Incremental loading is suspended.
2126 jest.advanceTimersByTime(500);
@@ -2490,7 +2417,13 @@ describe('ReactSuspenseList', () => {
2417 // This should leave the tree intact.
2418 await act(() => ReactNoop.render(<Foo updateList={true} />));
2419
2493 - assertLog(['A', 'Suspend! [B]', 'Loading B']);
2420 + assertLog([
2421 + 'A',
2422 + 'Suspend! [B]',
2423 + 'Loading B',
2424 +
2425 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [B]'] : []),
2426 + ]);
2427
2428 expect(ReactNoop).toMatchRenderedOutput(
2429 <>
@@ -2571,7 +2504,12 @@ describe('ReactSuspenseList', () => {
2504 expect(ReactNoop).toMatchRenderedOutput(<span>Loading A</span>);
2505
2506 // Try again on low-pri.
2574 - await waitForAll(['Suspend! [A]', 'Loading A']);
2507 + await waitForAll([
2508 + 'Suspend! [A]',
2509 + 'Loading A',
2510 +
2511 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [A]'] : []),
2512 + ]);
2513 expect(ReactNoop).toMatchRenderedOutput(<span>Loading A</span>);
2514 });
2515
@@ -2847,7 +2785,15 @@ describe('ReactSuspenseList', () => {
2785
2786 ReactNoop.render(<App suspendTail={true} />);
2787
2850 - await waitForAll(['App', 'A', 'B', 'Suspend! [C]', 'Fallback']);
2788 + await waitForAll([
2789 + 'App',
2790 + 'A',
2791 + 'B',
2792 + 'Suspend! [C]',
2793 + 'Fallback',
2794 +
2795 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [C]'] : []),
2796 + ]);
2797 expect(ReactNoop).toMatchRenderedOutput(
2798 <>
2799 <span>A</span>
@@ -2900,6 +2846,8 @@ describe('ReactSuspenseList', () => {
2846 'Fallback',
2847 // Lastly we render the tail.
2848 'Fallback',
2849 +
2850 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [C]'] : []),
2851 ]);
2852
2853 // Flush suspended time.
@@ -2914,7 +2862,9 @@ describe('ReactSuspenseList', () => {
2862 <span>Loading...</span>
2863 </>,
2864 );
2917 - expect(onRender).toHaveBeenCalledTimes(3);
2865 + expect(onRender).toHaveBeenCalledTimes(
2866 + gate('enableSiblingPrerendering') ? 4 : 3,
2867 + );
2868
2869 // The treeBaseDuration should be the time to render the first two
2870 // children and then two fallbacks.
@@ -2932,7 +2882,7 @@ describe('ReactSuspenseList', () => {
2882 'C',
2883 'Suspend! [D]',
2884
2935 - ...(gate('enableSiblingPrerendering') ? ['C', 'Suspend! [D]'] : []),
2885 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [D]'] : []),
2886 ]);
2887 expect(ReactNoop).toMatchRenderedOutput(
2888 <>
@@ -2942,12 +2892,22 @@ describe('ReactSuspenseList', () => {
2892 <span>Loading...</span>
2893 </>,
2894 );
2945 - expect(onRender).toHaveBeenCalledTimes(4);
2895
2947 - // actualDuration
2948 - expect(onRender.mock.calls[3][2]).toBe(5 + 12);
2949 - // treeBaseDuration
2950 - expect(onRender.mock.calls[3][3]).toBe(1 + 4 + 5 + 3);
2896 + if (gate('enableSiblingPrerendering')) {
2897 + expect(onRender).toHaveBeenCalledTimes(6);
2898 +
2899 + // actualDuration
2900 + expect(onRender.mock.calls[5][2]).toBe(12);
2901 + // treeBaseDuration
2902 + expect(onRender.mock.calls[5][3]).toBe(1 + 4 + 5 + 3);
2903 + } else {
2904 + expect(onRender).toHaveBeenCalledTimes(4);
2905 +
2906 + // actualDuration
2907 + expect(onRender.mock.calls[3][2]).toBe(5 + 12);
2908 + // treeBaseDuration
2909 + expect(onRender.mock.calls[3][3]).toBe(1 + 4 + 5 + 3);
2910 + }
2911 });
2912
2913 // @gate enableSuspenseList
@@ -3008,7 +2968,14 @@ describe('ReactSuspenseList', () => {
2968
2969 ReactNoop.render(<Foo />);
2970
3011 - await waitForAll(['Suspend! [A]', 'Loading A', 'Loading B', 'Loading C']);
2971 + await waitForAll([
2972 + 'Suspend! [A]',
2973 + 'Loading A',
2974 + 'Loading B',
2975 + 'Loading C',
2976 +
2977 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [A]'] : []),
2978 + ]);
2979
2980 expect(ReactNoop).toMatchRenderedOutput(
2981 <>
@@ -3023,7 +2990,7 @@ describe('ReactSuspenseList', () => {
2990 'A',
2991 'Suspend! [B]',
2992
3026 - ...(gate('enableSiblingPrerendering') ? ['A', 'Suspend! [B]'] : []),
2993 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [B]'] : []),
2994 ]);
2995 expect(ReactNoop).toMatchRenderedOutput(
2996 <>
packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js
+89 -18
@@ -135,7 +135,13 @@ describe('ReactSuspensePlaceholder', () => {
135 // Initial mount
136 ReactNoop.render(<App middleText="B" />);
137
138 - await waitForAll(['A', 'Suspend! [B]', 'Loading...']);
138 + await waitForAll([
139 + 'A',
140 + 'Suspend! [B]',
141 + 'Loading...',
142 +
143 + ...(gate('enableSiblingPrerendering') ? ['A', 'Suspend! [B]', 'C'] : []),
144 + ]);
145 expect(ReactNoop).toMatchRenderedOutput('Loading...');
146
147 await act(() => jest.advanceTimersByTime(1000));
@@ -151,7 +157,12 @@ describe('ReactSuspensePlaceholder', () => {
157
158 // Update
159 ReactNoop.render(<App middleText="B2" />);
154 - await waitForAll(['Suspend! [B2]', 'Loading...']);
160 + await waitForAll([
161 + 'Suspend! [B2]',
162 + 'Loading...',
163 +
164 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [B2]', 'C'] : []),
165 + ]);
166
167 // Time out the update
168 jest.advanceTimersByTime(750);
@@ -194,7 +205,13 @@ describe('ReactSuspensePlaceholder', () => {
205 // Initial mount
206 ReactNoop.render(<App middleText="B" />);
207
197 - await waitForAll(['A', 'Suspend! [B]', 'Loading...']);
208 + await waitForAll([
209 + 'A',
210 + 'Suspend! [B]',
211 + 'Loading...',
212 +
213 + ...(gate('enableSiblingPrerendering') ? ['A', 'Suspend! [B]', 'C'] : []),
214 + ]);
215
216 expect(ReactNoop).not.toMatchRenderedOutput('ABC');
217
@@ -204,7 +221,13 @@ describe('ReactSuspensePlaceholder', () => {
221
222 // Update
223 ReactNoop.render(<App middleText="B2" />);
207 - await waitForAll(['A', 'Suspend! [B2]', 'Loading...']);
224 + await waitForAll([
225 + 'A',
226 + 'Suspend! [B2]',
227 + 'Loading...',
228 +
229 + ...(gate('enableSiblingPrerendering') ? ['A', 'Suspend! [B2]', 'C'] : []),
230 + ]);
231 // Time out the update
232 jest.advanceTimersByTime(750);
233 await waitForAll([]);
@@ -237,7 +260,13 @@ describe('ReactSuspensePlaceholder', () => {
260 // Initial mount
261 ReactNoop.render(<App middleText="b" />);
262
240 - await waitForAll(['a', 'Suspend! [b]', 'Loading...']);
263 + await waitForAll([
264 + 'a',
265 + 'Suspend! [b]',
266 + 'Loading...',
267 +
268 + ...(gate('enableSiblingPrerendering') ? ['a', 'Suspend! [b]', 'c'] : []),
269 + ]);
270
271 expect(ReactNoop).toMatchRenderedOutput(<uppercase>LOADING...</uppercase>);
272
@@ -247,7 +276,13 @@ describe('ReactSuspensePlaceholder', () => {
276
277 // Update
278 ReactNoop.render(<App middleText="b2" />);
250 - await waitForAll(['a', 'Suspend! [b2]', 'Loading...']);
279 + await waitForAll([
280 + 'a',
281 + 'Suspend! [b2]',
282 + 'Loading...',
283 +
284 + ...(gate('enableSiblingPrerendering') ? ['a', 'Suspend! [b2]', 'c'] : []),
285 + ]);
286 // Time out the update
287 jest.advanceTimersByTime(750);
288 await waitForAll([]);
@@ -340,6 +375,10 @@ describe('ReactSuspensePlaceholder', () => {
375 'Suspending',
376 'Suspend! [Loaded]',
377 'Fallback',
378 +
379 + ...(gate('enableSiblingPrerendering')
380 + ? ['Suspending', 'Suspend! [Loaded]', 'Text']
381 + : []),
382 ]);
383 // Since this is initial render we immediately commit the fallback. Another test below
384 // deals with the update case where this suspends.
@@ -361,12 +400,22 @@ describe('ReactSuspensePlaceholder', () => {
400 'Text',
401 ]);
402 expect(ReactNoop).toMatchRenderedOutput('LoadedText');
364 - expect(onRender).toHaveBeenCalledTimes(2);
403
366 - // When the suspending data is resolved and our final UI is rendered,
367 - // both times should include the 8ms re-rendering Suspending and AsyncText.
368 - expect(onRender.mock.calls[1][2]).toBe(8);
369 - expect(onRender.mock.calls[1][3]).toBe(8);
404 + if (gate('enableSiblingPrerendering')) {
405 + expect(onRender).toHaveBeenCalledTimes(3);
406 +
407 + // When the suspending data is resolved and our final UI is rendered,
408 + // both times should include the 8ms re-rendering Suspending and AsyncText.
409 + expect(onRender.mock.calls[2][2]).toBe(8);
410 + expect(onRender.mock.calls[2][3]).toBe(8);
411 + } else {
412 + expect(onRender).toHaveBeenCalledTimes(2);
413 +
414 + // When the suspending data is resolved and our final UI is rendered,
415 + // both times should include the 8ms re-rendering Suspending and AsyncText.
416 + expect(onRender.mock.calls[1][2]).toBe(8);
417 + expect(onRender.mock.calls[1][3]).toBe(8);
418 + }
419 });
420 });
421
@@ -487,6 +536,10 @@ describe('ReactSuspensePlaceholder', () => {
536 'Suspending',
537 'Suspend! [Loaded]',
538 'Fallback',
539 +
540 + ...(gate('enableSiblingPrerendering')
541 + ? ['Suspending', 'Suspend! [Loaded]', 'Text']
542 + : []),
543 ]);
544 // Show the fallback UI.
545 expect(ReactNoop).toMatchRenderedOutput('Loading...');
@@ -526,9 +579,16 @@ describe('ReactSuspensePlaceholder', () => {
579 'Suspend! [Loaded]',
580 'Fallback',
581 'Suspend! [Sibling]',
582 +
583 + ...(gate('enableSiblingPrerendering')
584 + ? ['Suspending', 'Suspend! [Loaded]', 'New', 'Suspend! [Sibling]']
585 + : []),
586 ]);
587 expect(ReactNoop).toMatchRenderedOutput('Loading...');
531 - expect(onRender).toHaveBeenCalledTimes(3);
588 +
589 + expect(onRender).toHaveBeenCalledTimes(
590 + gate('enableSiblingPrerendering') ? 4 : 3,
591 + );
592
593 // Resolve the pending promise.
594 await act(async () => {
@@ -539,13 +599,24 @@ describe('ReactSuspensePlaceholder', () => {
599 ]);
600 await waitForAll(['Suspending', 'Loaded', 'New', 'Sibling']);
601 });
542 - expect(onRender).toHaveBeenCalledTimes(4);
602
544 - // When the suspending data is resolved and our final UI is rendered,
545 - // both times should include the 6ms rendering Text,
546 - // the 2ms rendering Suspending, and the 1ms rendering AsyncText.
547 - expect(onRender.mock.calls[3][2]).toBe(9);
548 - expect(onRender.mock.calls[3][3]).toBe(9);
603 + if (gate('enableSiblingPrerendering')) {
604 + expect(onRender).toHaveBeenCalledTimes(5);
605 +
606 + // When the suspending data is resolved and our final UI is rendered,
607 + // both times should include the 6ms rendering Text,
608 + // the 2ms rendering Suspending, and the 1ms rendering AsyncText.
609 + expect(onRender.mock.calls[4][2]).toBe(9);
610 + expect(onRender.mock.calls[4][3]).toBe(9);
611 + } else {
612 + expect(onRender).toHaveBeenCalledTimes(4);
613 +
614 + // When the suspending data is resolved and our final UI is rendered,
615 + // both times should include the 6ms rendering Text,
616 + // the 2ms rendering Suspending, and the 1ms rendering AsyncText.
617 + expect(onRender.mock.calls[3][2]).toBe(9);
618 + expect(onRender.mock.calls[3][3]).toBe(9);
619 + }
620 });
621 });
622 });
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
+245 -57
@@ -334,6 +334,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
334 'Loading A...',
335 'Suspend! [B]',
336 'Loading B...',
337 +
338 + ...(gate('enableSiblingPrerendering')
339 + ? ['Suspend! [A]', 'Suspend! [B]']
340 + : []),
341 ]);
342 expect(ReactNoop).toMatchRenderedOutput(
343 <>
@@ -345,7 +349,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
349 // Resolve first Suspense's promise so that it switches switches back to the
350 // normal view. The second Suspense should still show the placeholder.
351 await act(() => resolveText('A'));
348 - assertLog(['A']);
352 + assertLog([
353 + 'A',
354 +
355 + ...(gate('enableSiblingPrerendering')
356 + ? ['Suspend! [B]', 'Suspend! [B]']
357 + : []),
358 + ]);
359 expect(ReactNoop).toMatchRenderedOutput(
360 <>
361 <span prop="A" />
@@ -496,7 +506,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
506 }
507
508 ReactNoop.render(<App />);
499 - await waitForAll(['Suspend! [Result]', 'Loading...']);
509 + await waitForAll([
510 + 'Suspend! [Result]',
511 + 'Loading...',
512 +
513 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [Result]'] : []),
514 + ]);
515 expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
516
517 await act(() => rejectText('Result', new Error('Failed to load: Result')));
@@ -506,10 +521,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
521 // React retries one more time
522 'Error! [Result]',
523
509 - ...(gate('enableSiblingPrerendering')
510 - ? ['Error! [Result]', 'Error! [Result]']
511 - : []),
512 -
524 // Errored again on retry. Now handle it.
525 'Caught error: Failed to load: Result',
526 ]);
@@ -547,7 +558,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
558
559 // Initial mount
560 await act(() => ReactNoop.render(<App />));
550 - assertLog(['A', 'Suspend! [1]', 'Loading...']);
561 + assertLog([
562 + 'A',
563 + 'Suspend! [1]',
564 + 'Loading...',
565 +
566 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [1]'] : []),
567 + ]);
568
569 await act(() => resolveText('1'));
570 assertLog(['1']);
@@ -748,6 +765,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
765 // The async content suspends
766 'Suspend! [Outer content]',
767 'Loading outer...',
768 +
769 + ...(gate('enableSiblingPrerendering')
770 + ? [
771 + 'Suspend! [Outer content]',
772 + 'Suspend! [Inner content]',
773 + 'Loading inner...',
774 + ]
775 + : []),
776 ]);
777 // The outer loading state finishes immediately.
778 expect(ReactNoop).toMatchRenderedOutput(
@@ -763,10 +788,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
788 'Outer content',
789 'Suspend! [Inner content]',
790 'Loading inner...',
766 -
767 - ...(gate('enableSiblingPrerendering')
768 - ? ['Outer content', 'Suspend! [Inner content]', 'Loading inner...']
769 - : []),
791 ]);
792 // Don't commit the inner placeholder yet.
793 expect(ReactNoop).toMatchRenderedOutput(
@@ -928,7 +949,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
949 <AsyncText text="B" />
950 </Suspense>,
951 );
931 - await waitForAll(['Suspend! [A]', 'Loading...']);
952 + await waitForAll([
953 + 'Suspend! [A]',
954 + 'Loading...',
955 +
956 + ...(gate('enableSiblingPrerendering')
957 + ? ['Suspend! [A]', 'Suspend! [B]']
958 + : []),
959 + ]);
960 expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
961
962 await act(() => {
@@ -1038,7 +1066,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1066 }
1067
1068 ReactNoop.render(<App />);
1041 - await waitForAll(['Suspend! [A]']);
1069 + await waitForAll([
1070 + 'Suspend! [A]',
1071 +
1072 + ...(gate('enableSiblingPrerendering')
1073 + ? ['Suspend! [A]', 'Suspend! [B]', 'Suspend! [C]']
1074 + : []),
1075 + ]);
1076 expect(ReactNoop).toMatchRenderedOutput('Loading...');
1077
1078 await resolveText('A');
@@ -1701,6 +1735,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1735 // A suspends
1736 'Suspend! [A]',
1737 'Loading...',
1738 +
1739 + ...(gate('enableSiblingPrerendering')
1740 + ? ['Suspend! [A]', 'Suspend! [B]', 'Loading more...']
1741 + : []),
1742 ]);
1743 expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
1744
@@ -1715,6 +1753,8 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1753 // B suspends
1754 'Suspend! [B]',
1755 'Loading more...',
1756 +
1757 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [B]'] : []),
1758 ]);
1759
1760 // Because we've already been waiting for so long we've exceeded
@@ -1759,6 +1799,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1799 // A suspends
1800 'Suspend! [A]',
1801 'Loading...',
1802 +
1803 + ...(gate('enableSiblingPrerendering')
1804 + ? ['Suspend! [A]', 'Suspend! [B]', 'Loading more...']
1805 + : []),
1806 ]);
1807 expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
1808
@@ -1771,10 +1815,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1815 // B suspends
1816 'Suspend! [B]',
1817 'Loading more...',
1774 -
1775 - ...(gate('enableSiblingPrerendering')
1776 - ? ['A', 'Suspend! [B]', 'Loading more...']
1777 - : []),
1818 ]);
1819 // Because we've already been waiting for so long we can
1820 // wait a bit longer. Still nothing...
@@ -1841,6 +1881,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1881 'Loading A...',
1882 'Suspend! [B]',
1883 'Loading B...',
1884 +
1885 + ...(gate('enableSiblingPrerendering')
1886 + ? ['Suspend! [A]', 'Suspend! [B]']
1887 + : []),
1888 ]);
1889 expect(ReactNoop).toMatchRenderedOutput(
1890 <>
@@ -1985,14 +2029,24 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2029 ReactNoop.render(<App />);
2030 });
2031
1988 - assertLog(['Suspend! [A]']);
2032 + assertLog([
2033 + 'Suspend! [A]',
2034 +
2035 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [A]'] : []),
2036 + ]);
2037 expect(ReactNoop).toMatchRenderedOutput('Loading...');
2038
2039 await act(() => {
2040 ReactNoop.flushSync(() => showB());
2041 });
2042
1995 - assertLog(['Suspend! [A]']);
2043 + assertLog([
2044 + 'Suspend! [A]',
2045 +
2046 + ...(gate('enableSiblingPrerendering')
2047 + ? ['Suspend! [A]', 'Suspend! [B]']
2048 + : []),
2049 + ]);
2050 });
2051
2052 // TODO: flip to "warns" when this is implemented again.
@@ -2048,7 +2102,11 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2102 // also make sure lowpriority is okay
2103 await act(() => show(true));
2104
2051 - assertLog(['Suspend! [A]']);
2105 + assertLog([
2106 + 'Suspend! [A]',
2107 +
2108 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [A]'] : []),
2109 + ]);
2110 await resolveText('A');
2111
2112 expect(ReactNoop).toMatchRenderedOutput('Loading...');
@@ -2074,7 +2132,11 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2132 // also make sure lowpriority is okay
2133 await act(() => _setShow(true));
2134
2077 - assertLog(['Suspend! [A]']);
2135 + assertLog([
2136 + 'Suspend! [A]',
2137 +
2138 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [A]'] : []),
2139 + ]);
2140 await resolveText('A');
2141
2142 expect(ReactNoop).toMatchRenderedOutput('Loading...');
@@ -2098,7 +2160,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2160 }
2161
2162 ReactNoop.render(<Foo />);
2101 - await waitForAll(['Foo', 'Suspend! [A]', 'Initial load...']);
2163 + await waitForAll([
2164 + 'Foo',
2165 + 'Suspend! [A]',
2166 + 'Initial load...',
2167 +
2168 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [A]', 'B'] : []),
2169 + ]);
2170 expect(ReactNoop).toMatchRenderedOutput(<span prop="Initial load..." />);
2171
2172 // Eventually we resolve and show the data.
@@ -2113,7 +2181,15 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2181
2182 // Update to show C
2183 ReactNoop.render(<Foo showC={true} />);
2116 - await waitForAll(['Foo', 'A', 'Suspend! [C]', 'Updating...', 'B']);
2184 + await waitForAll([
2185 + 'Foo',
2186 + 'A',
2187 + 'Suspend! [C]',
2188 + 'Updating...',
2189 + 'B',
2190 +
2191 + ...(gate('enableSiblingPrerendering') ? ['A', 'Suspend! [C]'] : []),
2192 + ]);
2193 // Flush to skip suspended time.
2194 Scheduler.unstable_advanceTime(600);
2195 await advanceTimers(600);
@@ -2160,6 +2236,8 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2236 'Suspend! [A]',
2237 'B',
2238 // null
2239 +
2240 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [A]'] : []),
2241 ]);
2242 expect(ReactNoop).toMatchRenderedOutput(<span prop="B" />);
2243
@@ -2181,6 +2259,8 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2259 'Suspend! [C]',
2260 // null
2261 'B',
2262 +
2263 + ...(gate('enableSiblingPrerendering') ? ['A', 'Suspend! [C]'] : []),
2264 ]);
2265 // Flush to skip suspended time.
2266 Scheduler.unstable_advanceTime(600);
@@ -2223,7 +2303,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2303 }
2304
2305 ReactNoop.render(<Foo />);
2226 - await waitForAll(['Foo', 'A', 'Suspend! [B]', 'Loading B...']);
2306 + await waitForAll([
2307 + 'Foo',
2308 + 'A',
2309 + 'Suspend! [B]',
2310 + 'Loading B...',
2311 +
2312 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [B]'] : []),
2313 + ]);
2314 // Flush to skip suspended time.
2315 Scheduler.unstable_advanceTime(600);
2316 await advanceTimers(600);
@@ -2303,6 +2390,8 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2390 'A',
2391 'Suspend! [B]',
2392 // Null
2393 +
2394 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [B]'] : []),
2395 ]);
2396 // Still suspended.
2397 expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
@@ -2328,7 +2417,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2417 // Initial render.
2418 React.startTransition(() => ReactNoop.render(<App page="A" />));
2419
2331 - await waitForAll(['Suspend! [A]', 'Loading...']);
2420 + await waitForAll([
2421 + 'Suspend! [A]',
2422 + 'Loading...',
2423 +
2424 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [A]'] : []),
2425 + ]);
2426 // Only a short time is needed to unsuspend the initial loading state.
2427 Scheduler.unstable_advanceTime(400);
2428 await advanceTimers(400);
@@ -2377,7 +2471,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2471 await act(async () => {
2472 React.startTransition(() => transitionToPage('A'));
2473
2380 - await waitForAll(['Suspend! [A]', 'Loading...']);
2474 + await waitForAll([
2475 + 'Suspend! [A]',
2476 + 'Loading...',
2477 +
2478 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [A]'] : []),
2479 + ]);
2480 // Only a short time is needed to unsuspend the initial loading state.
2481 Scheduler.unstable_advanceTime(400);
2482 await advanceTimers(400);
@@ -2432,7 +2531,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2531 await act(async () => {
2532 React.startTransition(() => transitionToPage('A'));
2533
2435 - await waitForAll(['Suspend! [A]', 'Loading...']);
2534 + await waitForAll([
2535 + 'Suspend! [A]',
2536 + 'Loading...',
2537 +
2538 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [A]'] : []),
2539 + ]);
2540 // Only a short time is needed to unsuspend the initial loading state.
2541 Scheduler.unstable_advanceTime(400);
2542 await advanceTimers(400);
@@ -2476,7 +2580,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2580 // Initial render.
2581 React.startTransition(() => ReactNoop.render(<App page="A" />));
2582
2479 - await waitForAll(['Suspend! [A]', 'Loading...']);
2583 + await waitForAll([
2584 + 'Suspend! [A]',
2585 + 'Loading...',
2586 +
2587 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [A]'] : []),
2588 + ]);
2589 // Only a short time is needed to unsuspend the initial loading state.
2590 Scheduler.unstable_advanceTime(400);
2591 await advanceTimers(400);
@@ -2536,7 +2645,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2645 await act(async () => {
2646 React.startTransition(() => transitionToPage('A'));
2647
2539 - await waitForAll(['Suspend! [A]', 'Loading...']);
2648 + await waitForAll([
2649 + 'Suspend! [A]',
2650 + 'Loading...',
2651 +
2652 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [A]'] : []),
2653 + ]);
2654 // Only a short time is needed to unsuspend the initial loading state.
2655 Scheduler.unstable_advanceTime(400);
2656 await advanceTimers(400);
@@ -2606,7 +2720,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2720 await act(async () => {
2721 React.startTransition(() => transitionToPage('A'));
2722
2609 - await waitForAll(['Suspend! [A]', 'Loading...']);
2723 + await waitForAll([
2724 + 'Suspend! [A]',
2725 + 'Loading...',
2726 +
2727 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [A]'] : []),
2728 + ]);
2729 // Only a short time is needed to unsuspend the initial loading state.
2730 Scheduler.unstable_advanceTime(400);
2731 await advanceTimers(400);
@@ -2667,7 +2786,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2786
2787 // Initial render.
2788 ReactNoop.render(<App page="A" />);
2670 - await waitForAll(['Hi!', 'Suspend! [A]', 'Loading...']);
2789 + await waitForAll([
2790 + 'Hi!',
2791 + 'Suspend! [A]',
2792 + 'Loading...',
2793 +
2794 + ...(gate('enableSiblingPrerendering') ? ['Hi!', 'Suspend! [A]'] : []),
2795 + ]);
2796 await act(() => resolveText('A'));
2797 assertLog(['Hi!', 'A']);
2798 expect(ReactNoop).toMatchRenderedOutput(
@@ -2988,6 +3113,14 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3113 },
3114 );
3115
3116 + // TODO: This test is substantially different when sibling prerendering is
3117 + // enabled because we never work on Idle updates if there are pending retries.
3118 + // This was already an issue before the enableSiblingPrerendering change but
3119 + // it's exacerbated by the fact that we schedule a retry immediately. I'm not
3120 + // going to bother to update this test for now, though, because Idle updates
3121 + // aren't actually used and should probably just be deleted unless/until we
3122 + // finish the feature. Feel free to delete if needed.
3123 + // @gate !enableSiblingPrerendering
3124 // @gate enableLegacyCache
3125 it(
3126 'multiple updates originating inside a Suspense boundary at different ' +
@@ -3086,7 +3219,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3219 await act(async () => {
3220 // Schedule an update inside the Suspense boundary that suspends.
3221 setAppText('B');
3089 - await waitForAll(['Suspend! [B]', 'Loading...']);
3222 + await waitForAll([
3223 + 'Suspend! [B]',
3224 + 'Loading...',
3225 +
3226 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [B]'] : []),
3227 + ]);
3228 });
3229
3230 expect(root).toMatchRenderedOutput(
@@ -3122,6 +3260,8 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3260
3261 // Then complete the update to the fallback.
3262 'Still loading...',
3263 +
3264 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [C]'] : []),
3265 ]);
3266 expect(root).toMatchRenderedOutput(
3267 <>
@@ -3182,7 +3322,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3322 await act(() => {
3323 setText('C');
3324 });
3185 - assertLog(['Suspend! [C]', 'Loading...']);
3325 + assertLog([
3326 + 'Suspend! [C]',
3327 + 'Loading...',
3328 +
3329 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [C]'] : []),
3330 + ]);
3331
3332 // Commit. This will insert a fragment fiber to wrap around the component
3333 // that triggered the update.
@@ -3259,7 +3404,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3404 await act(() => {
3405 setText('C');
3406 });
3262 - assertLog(['Suspend! [C]', 'Loading...']);
3407 + assertLog([
3408 + 'Suspend! [C]',
3409 + 'Loading...',
3410 +
3411 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [C]'] : []),
3412 + ]);
3413
3414 // Commit. This will insert a fragment fiber to wrap around the component
3415 // that triggered the update.
@@ -3288,7 +3438,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3438 });
3439 // Even though the fragment fiber is not part of the return path, we should
3440 // be able to finish rendering.
3291 - assertLog(['Suspend! [D]', 'E']);
3441 + assertLog([
3442 + 'Suspend! [D]',
3443 +
3444 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [D]'] : []),
3445 +
3446 + 'E',
3447 + ]);
3448 expect(root).toMatchRenderedOutput(<span prop="E" />);
3449 },
3450 );
@@ -3374,6 +3530,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3530 'Outer step: 0',
3531 'Suspend! [Inner text: B]',
3532 'Loading...',
3533 +
3534 + ...(gate('enableSiblingPrerendering')
3535 + ? ['Suspend! [Inner text: B]', 'Inner step: 0']
3536 + : []),
3537 ]);
3538 // Commit the placeholder
3539 await advanceTimers(250);
@@ -3402,6 +3562,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3562 'Outer step: 1',
3563 'Suspend! [Inner text: B]',
3564 'Loading...',
3565 +
3566 + ...(gate('enableSiblingPrerendering')
3567 + ? ['Suspend! [Inner text: B]', 'Inner step: 1']
3568 + : []),
3569 ]);
3570 expect(root).toMatchRenderedOutput(
3571 <>
@@ -3492,7 +3656,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3656 await act(() => {
3657 setText('B');
3658 });
3495 - assertLog(['Outer: B0', 'Suspend! [Inner: B0]', 'Loading...']);
3659 + assertLog([
3660 + 'Outer: B0',
3661 + 'Suspend! [Inner: B0]',
3662 + 'Loading...',
3663 +
3664 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [Inner: B0]'] : []),
3665 + ]);
3666 // Commit the placeholder
3667 await advanceTimers(250);
3668 expect(root).toMatchRenderedOutput(
@@ -3520,10 +3690,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3690 );
3691 });
3692
3523 - // This regression test relies on subtle implementation details that happen to
3524 - // rely on sibling prerendering being disabled. Not going to bother to rewrite
3525 - // it for now; maybe once we land the experiment.
3526 - // @gate !enableSiblingPrerendering
3693 // @gate enableLegacyCache
3694 it('regression: ping at high priority causes update to be dropped', async () => {
3695 const {useState, useTransition} = React;
@@ -3737,8 +3903,16 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3903 });
3904 });
3905
3906 + // TODO: This test is substantially different when sibling prerendering is
3907 + // enabled because we never work on Idle updates if there are pending retries.
3908 + // This was already an issue before the enableSiblingPrerendering change but
3909 + // it's exacerbated by the fact that we schedule a retry immediately. I'm not
3910 + // going to bother to update this test for now, though, because Idle updates
3911 + // aren't actually used and should probably just be deleted unless/until we
3912 + // finish the feature. Feel free to delete if needed.
3913 + // @gate !enableSiblingPrerendering
3914 // @gate enableLegacyCache
3741 - it('regression: #18657', async () => {
3915 + it('regression related to Idle updates (outdated experiment): #18657', async () => {
3916 const {useState} = React;
3917
3918 let setText;
@@ -3822,7 +3996,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3996 </>,
3997 );
3998 });
3825 - assertLog(['A', 'Suspend! [Async]', 'Loading...']);
3999 + assertLog([
4000 + 'A',
4001 + 'Suspend! [Async]',
4002 + 'Loading...',
4003 +
4004 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [Async]'] : []),
4005 + ]);
4006 expect(root).toMatchRenderedOutput(
4007 <>
4008 <span prop="A" />
@@ -3889,7 +4069,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
4069 await act(() => {
4070 root.render(<App show={true} />);
4071 });
3892 - assertLog(['Suspend! [Async]', 'Loading...']);
4072 + assertLog([
4073 + 'Suspend! [Async]',
4074 + 'Loading...',
4075 +
4076 + ...(gate('enableSiblingPrerendering') ? ['Suspend! [Async]'] : []),
4077 + ]);
4078 expect(root).toMatchRenderedOutput(
4079 <>
4080 <span hidden={true} prop="Child" />
@@ -3960,7 +4145,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
4145 // used to cause a crash.
4146 const thenable = {
4147 then(resolve) {
3963 - resolve('hi');
4148 + resolve('B');
4149 },
4150 status: 'pending',
4151 };
@@ -3970,21 +4155,21 @@ describe('ReactSuspenseWithNoopRenderer', () => {
4155 thenable.status = 'fulfilled';
4156 throw thenable;
4157 }
3973 - return <Text text="Hi" />;
4158 + return <Text text="B" />;
4159 }
4160
4161 function App({showMore}) {
4162 return (
4163 <div>
3979 - <Suspense fallback={<Text text="Loading..." />}>
4164 + <Suspense fallback={<Text text="Loading A..." />}>
4165 {showMore ? (
4166 <>
3982 - <AsyncText text="Async" />
4167 + <AsyncText text="A" />
4168 </>
4169 ) : null}
4170 </Suspense>
4171 {showMore ? (
3987 - <Suspense>
4172 + <Suspense fallback={<Text text="Loading B..." />}>
4173 <ImmediatelyPings />
4174 </Suspense>
4175 ) : null}
@@ -4015,15 +4200,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
4200 // The fix was to check if we're in the render phase before calling
4201 // `prepareFreshStack`.
4202 await act(() => {
4018 - root.render(<App showMore={true} />);
4203 + startTransition(() => root.render(<App showMore={true} />));
4204 });
4020 - assertLog(['Suspend! [Async]', 'Loading...', 'Hi']);
4021 - expect(root).toMatchRenderedOutput(
4022 - <div>
4023 - <span prop="Loading..." />
4024 - <span prop="Hi" />
4025 - </div>,
4026 - );
4205 + assertLog(['Suspend! [A]', 'Loading A...', 'Loading B...']);
4206 + expect(root).toMatchRenderedOutput(<div />);
4207 },
4208 );
4209
@@ -4060,7 +4240,15 @@ describe('ReactSuspenseWithNoopRenderer', () => {
4240
4241 const root = ReactNoop.createRoot();
4242 root.render(<App />);
4063 - await waitForAll(['1', 'Suspend! [Async]', 'Loading...']);
4243 + await waitForAll([
4244 + '1',
4245 + 'Suspend! [Async]',
4246 + 'Loading...',
4247 +
4248 + ...(gate('enableSiblingPrerendering')
4249 + ? ['Suspend! [Async]', 'A', 'B', 'C']
4250 + : []),
4251 + ]);
4252 expect(root).toMatchRenderedOutput(
4253 <>
4254 <span prop="1" />
packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js
+114 -5
@@ -441,6 +441,7 @@ describe('ReactInteractionTracing', () => {
441 await waitForAll([
442 'Suspend [Page Two]',
443 'Loading...',
444 + ...(gate('enableSiblingPrerendering') ? ['Suspend [Page Two]'] : []),
445 'onTransitionStart(page transition, 1000)',
446 'onTransitionProgress(page transition, 1000, 2000, [suspense page])',
447 ]);
@@ -531,6 +532,7 @@ describe('ReactInteractionTracing', () => {
532 await waitForAll([
533 'Suspend [Page Two]',
534 'Loading...',
535 + ...(gate('enableSiblingPrerendering') ? ['Suspend [Page Two]'] : []),
536 'onTransitionStart(page transition, 1000)',
537 'onTransitionProgress(page transition, 1000, 1000, [suspense page])',
538 ]);
@@ -549,6 +551,7 @@ describe('ReactInteractionTracing', () => {
551 'Suspend [Show Text]',
552 'Show Text Loading...',
553 'Page Two',
554 + ...(gate('enableSiblingPrerendering') ? ['Suspend [Show Text]'] : []),
555 'onTransitionStart(text transition, 2000)',
556 'onTransitionProgress(text transition, 2000, 2000, [show text])',
557 ]);
@@ -638,6 +641,7 @@ describe('ReactInteractionTracing', () => {
641 await waitForAll([
642 'Suspend [Page Two]',
643 'Loading...',
644 + ...(gate('enableSiblingPrerendering') ? ['Suspend [Page Two]'] : []),
645 'onTransitionStart(page transition, 1000)',
646 'onTransitionProgress(page transition, 1000, 2000, [suspense page])',
647 ]);
@@ -651,6 +655,9 @@ describe('ReactInteractionTracing', () => {
655 'Show Text Loading...',
656 'Suspend [Page Two]',
657 'Loading...',
658 + ...(gate('enableSiblingPrerendering')
659 + ? ['Suspend [Show Text]', 'Suspend [Page Two]']
660 + : []),
661 'onTransitionStart(show text, 2000)',
662 'onTransitionProgress(show text, 2000, 2000, [show text])',
663 ]);
@@ -753,6 +760,15 @@ describe('ReactInteractionTracing', () => {
760 await waitForAll([
761 'Suspend [Page Two]',
762 'Loading...',
763 + ...(gate('enableSiblingPrerendering')
764 + ? [
765 + 'Suspend [Page Two]',
766 + 'Suspend [Show Text One]',
767 + 'Show Text One Loading...',
768 + 'Suspend [Show Text Two]',
769 + 'Show Text Two Loading...',
770 + ]
771 + : []),
772 'onTransitionStart(page transition, 1000)',
773 'onTransitionProgress(page transition, 1000, 2000, [suspense page])',
774 ]);
@@ -767,6 +783,9 @@ describe('ReactInteractionTracing', () => {
783 'Show Text One Loading...',
784 'Suspend [Show Text Two]',
785 'Show Text Two Loading...',
786 + ...(gate('enableSiblingPrerendering')
787 + ? ['Suspend [Show Text One]', 'Suspend [Show Text Two]']
788 + : []),
789 'onTransitionProgress(page transition, 1000, 3000, [show text one, show text two])',
790 ]);
791
@@ -879,6 +898,15 @@ describe('ReactInteractionTracing', () => {
898 await waitForAll([
899 'Suspend [Page Two]',
900 'Loading...',
901 + ...(gate('enableSiblingPrerendering')
902 + ? [
903 + 'Suspend [Page Two]',
904 + 'Suspend [Show Text One]',
905 + 'Show Text One Loading...',
906 + 'Suspend [Show Text]',
907 + 'Show Text Loading...',
908 + ]
909 + : []),
910 'onTransitionStart(navigate, 1000)',
911 'onTransitionStart(show text one, 1000)',
912 'onTransitionProgress(navigate, 1000, 2000, [suspense page])',
@@ -894,6 +922,9 @@ describe('ReactInteractionTracing', () => {
922 'Show Text One Loading...',
923 'Suspend [Show Text]',
924 'Show Text Loading...',
925 + ...(gate('enableSiblingPrerendering')
926 + ? ['Suspend [Show Text One]', 'Suspend [Show Text]']
927 + : []),
928 'onTransitionProgress(navigate, 1000, 3000, [show text one, <null>])',
929 'onTransitionProgress(show text one, 1000, 3000, [show text one, <null>])',
930 ]);
@@ -910,6 +941,13 @@ describe('ReactInteractionTracing', () => {
941 'Show Text Loading...',
942 'Suspend [Show Text Two]',
943 'Show Text Two Loading...',
944 + ...(gate('enableSiblingPrerendering')
945 + ? [
946 + 'Suspend [Show Text One]',
947 + 'Suspend [Show Text]',
948 + 'Suspend [Show Text Two]',
949 + ]
950 + : []),
951 'onTransitionStart(show text two, 3000)',
952 'onTransitionProgress(show text two, 3000, 4000, [show text two])',
953 ]);
@@ -1114,6 +1152,9 @@ describe('ReactInteractionTracing', () => {
1152 await waitForAll([
1153 'Suspend [Page Two]',
1154 'Loading...',
1155 + ...(gate('enableSiblingPrerendering')
1156 + ? ['Suspend [Page Two]', 'Suspend [Marker Text]', 'Loading...']
1157 + : []),
1158 'onTransitionStart(page transition, 1000)',
1159 ]);
1160
@@ -1125,6 +1166,7 @@ describe('ReactInteractionTracing', () => {
1166 'Page Two',
1167 'Suspend [Marker Text]',
1168 'Loading...',
1169 + ...(gate('enableSiblingPrerendering') ? ['Suspend [Marker Text]'] : []),
1170 'onMarkerProgress(page transition, async marker, 1000, 3000, [marker suspense])',
1171 'onMarkerComplete(page transition, sync marker, 1000, 3000)',
1172 ]);
@@ -1230,6 +1272,15 @@ describe('ReactInteractionTracing', () => {
1272 await waitForAll([
1273 'Suspend [Outer Text]',
1274 'Outer...',
1275 + ...(gate('enableSiblingPrerendering')
1276 + ? [
1277 + 'Suspend [Outer Text]',
1278 + 'Suspend [Inner Text One]',
1279 + 'Inner One...',
1280 + 'Suspend [Inner Text Two]',
1281 + 'Inner Two...',
1282 + ]
1283 + : []),
1284 'onTransitionStart(page transition, 1000)',
1285 'onMarkerProgress(page transition, outer marker, 1000, 2000, [outer])',
1286 ]);
@@ -1247,6 +1298,9 @@ describe('ReactInteractionTracing', () => {
1298 'Suspend [Inner Text One]',
1299 'Inner One...',
1300 'Inner Text Two',
1301 + ...(gate('enableSiblingPrerendering')
1302 + ? ['Suspend [Inner Text One]']
1303 + : []),
1304 'onMarkerProgress(page transition, outer marker, 1000, 4000, [inner one])',
1305 'onMarkerComplete(page transition, marker two, 1000, 4000)',
1306 ]);
@@ -1484,6 +1538,9 @@ describe('ReactInteractionTracing', () => {
1538 'Loading...',
1539 'Suspend [Sibling Text]',
1540 'Sibling Loading...',
1541 + ...(gate('enableSiblingPrerendering')
1542 + ? ['Suspend [Page Two]', 'Suspend [Sibling Text]']
1543 + : []),
1544 'onTransitionStart(transition one, 1000)',
1545 'onMarkerProgress(transition one, parent, 1000, 2000, [suspense page, suspense sibling])',
1546 'onMarkerProgress(transition one, marker one, 1000, 2000, [suspense page])',
@@ -1499,6 +1556,9 @@ describe('ReactInteractionTracing', () => {
1556 'Loading...',
1557 'Suspend [Sibling Text]',
1558 'Sibling Loading...',
1559 + ...(gate('enableSiblingPrerendering')
1560 + ? ['Suspend [Page Two]', 'Suspend [Sibling Text]']
1561 + : []),
1562 'onMarkerProgress(transition one, parent, 1000, 3000, [suspense sibling])',
1563 'onMarkerIncomplete(transition one, marker one, 1000, [{endTime: 3000, name: marker one, type: marker}, {endTime: 3000, name: suspense page, type: suspense}])',
1564 'onMarkerIncomplete(transition one, parent, 1000, [{endTime: 3000, name: marker one, type: marker}, {endTime: 3000, name: suspense page, type: suspense}])',
@@ -1512,6 +1572,9 @@ describe('ReactInteractionTracing', () => {
1572 'Loading...',
1573 'Suspend [Sibling Text]',
1574 'Sibling Loading...',
1575 + ...(gate('enableSiblingPrerendering')
1576 + ? ['Suspend [Page Two]', 'Suspend [Sibling Text]']
1577 + : []),
1578 ]);
1579 });
1580
@@ -1633,6 +1696,9 @@ describe('ReactInteractionTracing', () => {
1696 'Loading One...',
1697 'Suspend [Page Two]',
1698 'Loading Two...',
1699 + ...(gate('enableSiblingPrerendering')
1700 + ? ['Suspend [Page One]', 'Suspend [Page Two]']
1701 + : []),
1702 'onTransitionStart(transition, 1000)',
1703 'onMarkerProgress(transition, parent, 1000, 2000, [suspense one, suspense two])',
1704 'onMarkerProgress(transition, one, 1000, 2000, [suspense one])',
@@ -1646,6 +1712,7 @@ describe('ReactInteractionTracing', () => {
1712 await waitForAll([
1713 'Suspend [Page Two]',
1714 'Loading Two...',
1715 + ...(gate('enableSiblingPrerendering') ? ['Suspend [Page Two]'] : []),
1716 'onMarkerProgress(transition, parent, 1000, 3000, [suspense two])',
1717 'onMarkerIncomplete(transition, one, 1000, [{endTime: 3000, name: one, type: marker}, {endTime: 3000, name: suspense one, type: suspense}])',
1718 'onMarkerIncomplete(transition, parent, 1000, [{endTime: 3000, name: one, type: marker}, {endTime: 3000, name: suspense one, type: suspense}])',
@@ -1772,6 +1839,14 @@ describe('ReactInteractionTracing', () => {
1839 'Loading One...',
1840 'Suspend [Page Two]',
1841 'Loading Two...',
1842 + ...(gate('enableSiblingPrerendering')
1843 + ? [
1844 + 'Suspend [Page One]',
1845 + 'Suspend [Child]',
1846 + 'Loading Child...',
1847 + 'Suspend [Page Two]',
1848 + ]
1849 + : []),
1850 'onTransitionStart(transition, 1000)',
1851 'onMarkerProgress(transition, parent, 1000, 2000, [suspense one, suspense two])',
1852 'onMarkerProgress(transition, one, 1000, 2000, [suspense one])',
@@ -1786,6 +1861,7 @@ describe('ReactInteractionTracing', () => {
1861 'Page One',
1862 'Suspend [Child]',
1863 'Loading Child...',
1864 + ...(gate('enableSiblingPrerendering') ? ['Suspend [Child]'] : []),
1865 'onMarkerProgress(transition, parent, 1000, 3000, [suspense two, suspense child])',
1866 'onMarkerProgress(transition, one, 1000, 3000, [suspense child])',
1867 'onMarkerComplete(transition, page one, 1000, 3000)',
@@ -1798,6 +1874,7 @@ describe('ReactInteractionTracing', () => {
1874 await waitForAll([
1875 'Suspend [Page Two]',
1876 'Loading Two...',
1877 + ...(gate('enableSiblingPrerendering') ? ['Suspend [Page Two]'] : []),
1878 // "suspense one" has unsuspended so shouldn't be included
1879 // tracing marker "page one" has completed so shouldn't be included
1880 // all children of "suspense child" haven't yet been rendered so shouldn't be included
@@ -1895,6 +1972,7 @@ describe('ReactInteractionTracing', () => {
1972
1973 await waitForAll([
1974 'Suspend [Child]',
1975 + ...(gate('enableSiblingPrerendering') ? ['Suspend [Child]'] : []),
1976 'onTransitionStart(transition, 0)',
1977 'onMarkerProgress(transition, parent, 0, 1000, [child])',
1978 'onTransitionProgress(transition, 0, 1000, [child])',
@@ -1905,14 +1983,23 @@ describe('ReactInteractionTracing', () => {
1983 await advanceTimers(1000);
1984 // This appended child isn't part of the transition so we
1985 // don't call any callback
1908 - await waitForAll(['Suspend [Appended child]', 'Suspend [Child]']);
1986 + await waitForAll([
1987 + 'Suspend [Appended child]',
1988 + 'Suspend [Child]',
1989 + ...(gate('enableSiblingPrerendering')
1990 + ? ['Suspend [Appended child]', 'Suspend [Child]']
1991 + : []),
1992 + ]);
1993
1994 // This deleted child isn't part of the transition so we
1995 // don't call any callbacks
1996 root.render(<App show={false} />);
1997 ReactNoop.expire(1000);
1998 await advanceTimers(1000);
1915 - await waitForAll(['Suspend [Child]']);
1999 + await waitForAll([
2000 + 'Suspend [Child]',
2001 + ...(gate('enableSiblingPrerendering') ? ['Suspend [Child]'] : []),
2002 + ]);
2003
2004 await resolveText('Child');
2005 ReactNoop.expire(1000);
@@ -2013,6 +2100,7 @@ describe('ReactInteractionTracing', () => {
2100
2101 assertLog([
2102 'Suspend [Child]',
2103 + ...(gate('enableSiblingPrerendering') ? ['Suspend [Child]'] : []),
2104 'onTransitionStart(transition one, 0)',
2105 'onMarkerProgress(transition one, parent, 0, 1000, [child])',
2106 'onTransitionProgress(transition one, 0, 1000, [child])',
@@ -2033,6 +2121,9 @@ describe('ReactInteractionTracing', () => {
2121 assertLog([
2122 'Suspend [Appended child]',
2123 'Suspend [Child]',
2124 + ...(gate('enableSiblingPrerendering')
2125 + ? ['Suspend [Appended child]', 'Suspend [Child]']
2126 + : []),
2127 'onTransitionStart(transition two, 1000)',
2128 'onMarkerProgress(transition two, appended child, 1000, 2000, [appended child])',
2129 'onTransitionProgress(transition two, 1000, 2000, [appended child])',
@@ -2046,6 +2137,7 @@ describe('ReactInteractionTracing', () => {
2137
2138 assertLog([
2139 'Suspend [Child]',
2140 + ...(gate('enableSiblingPrerendering') ? ['Suspend [Child]'] : []),
2141 'onMarkerProgress(transition two, appended child, 1000, 3000, [])',
2142 'onMarkerIncomplete(transition two, appended child, 1000, [{endTime: 3000, name: appended child, type: suspense}])',
2143 ]);
@@ -2201,9 +2293,20 @@ describe('ReactInteractionTracing', () => {
2293 assertLog([
2294 'Suspend [Text]',
2295 'Loading...',
2204 - 'Suspend [Hidden Text]',
2205 - 'Hidden Loading...',
2206 - 'onTransitionStart(transition, 0)',
2296 +
2297 + ...(gate('enableSiblingPrerendering')
2298 + ? [
2299 + 'Suspend [Text]',
2300 + 'onTransitionStart(transition, 0)',
2301 +
2302 + 'Suspend [Hidden Text]',
2303 + 'Hidden Loading...',
2304 + ]
2305 + : [
2306 + 'Suspend [Hidden Text]',
2307 + 'Hidden Loading...',
2308 + 'onTransitionStart(transition, 0)',
2309 + ]),
2310 ]);
2311
2312 await act(() => {
@@ -2269,6 +2372,7 @@ describe('ReactInteractionTracing', () => {
2372 assertLog([
2373 'Suspend [Page Two]',
2374 'Loading...',
2375 + ...(gate('enableSiblingPrerendering') ? ['Suspend [Page Two]'] : []),
2376 'onTransitionStart(page transition, 0)',
2377 'onTransitionProgress(page transition, 0, 1000, [suspense page])',
2378 ]);
@@ -2342,8 +2446,10 @@ describe('ReactInteractionTracing', () => {
2446 'Text',
2447 'Suspend [Text Two]',
2448 'Loading Two...',
2449 + ...(gate('enableSiblingPrerendering') ? ['Suspend [Text Two]'] : []),
2450 'onTransitionStart(transition, 0)',
2451 'onTransitionProgress(transition, 0, 1000, [two])',
2452 + ...(gate('enableSiblingPrerendering') ? ['Suspend [Text Two]'] : []),
2453 ]);
2454
2455 await act(() => {
@@ -2417,6 +2523,9 @@ describe('ReactInteractionTracing', () => {
2523 'Loading one...',
2524 'Suspend [Text two]',
2525 'Loading two...',
2526 + ...(gate('enableSiblingPrerendering')
2527 + ? ['Suspend [Text one]', 'Suspend [Text two]']
2528 + : []),
2529 'onTransitionStart(transition one, 0) /root one/',
2530 'onTransitionProgress(transition one, 0, 1000, [one]) /root one/',
2531 'onTransitionStart(transition two, 0) /root two/',
packages/react-reconciler/src/__tests__/ReactUse-test.js
+15 -16
@@ -191,7 +191,12 @@ describe('ReactUse', () => {
191 await act(() => {
192 root.render(<App />);
193 });
194 - assertLog(['Suspend!', 'Loading...']);
194 + assertLog([
195 + 'Suspend!',
196 + 'Loading...',
197 +
198 + ...(gate('enableSiblingPrerendering') ? ['Suspend!'] : []),
199 + ]);
200 expect(root).toMatchRenderedOutput('Loading...');
201 });
202
@@ -1060,31 +1065,25 @@ describe('ReactUse', () => {
1065 </Suspense>,
1066 );
1067 });
1063 - assertLog(['(Loading A...)']);
1064 - expect(root).toMatchRenderedOutput('(Loading A...)');
1065 -
1066 - await act(() => {
1067 - resolveTextRequests('A');
1068 - });
1068 assertLog([
1070 - 'A',
1071 - '(Loading B...)',
1069 + '(Loading A...)',
1070
1071 ...(gate('enableSiblingPrerendering')
1074 - ? ['A', '(Loading C...)', '(Loading B...)']
1072 + ? ['(Loading C...)', '(Loading B...)']
1073 : []),
1074 ]);
1075 + expect(root).toMatchRenderedOutput('(Loading A...)');
1076 +
1077 + await act(() => {
1078 + resolveTextRequests('A');
1079 + });
1080 + assertLog(['A', '(Loading B...)']);
1081 expect(root).toMatchRenderedOutput('A(Loading B...)');
1082
1083 await act(() => {
1084 resolveTextRequests('B');
1085 });
1082 - assertLog([
1083 - 'B',
1084 - '(Loading C...)',
1085 -
1086 - ...(gate('enableSiblingPrerendering') ? ['B', '(Loading C...)'] : []),
1087 - ]);
1086 + assertLog(['B', '(Loading C...)']);
1087 expect(root).toMatchRenderedOutput('AB(Loading C...)');
1088
1089 await act(() => {
packages/react-reconciler/src/__tests__/StrictEffectsMode-test.js
+8
@@ -907,6 +907,10 @@ describe('StrictEffectsMode', () => {
907 'Child suspended',
908 'Fallback',
909 'Fallback',
910 +
911 + ...(gate('enableSiblingPrerendering')
912 + ? ['Child rendered', 'Child suspended']
913 + : []),
914 ]);
915
916 log = [];
@@ -928,6 +932,10 @@ describe('StrictEffectsMode', () => {
932 'Fallback',
933 'Parent dep destroy',
934 'Parent dep create',
935 +
936 + ...(gate('enableSiblingPrerendering')
937 + ? ['Child rendered', 'Child suspended']
938 + : []),
939 ]);
940
941 log = [];