Prerender during same pass if blocked anyway (#30879)

If something suspends in the shell — i.e. we won't replace the suspended
content with a fallback — we might as well prerender the siblings during
the current render pass, instead of spawning a separate prerender pass.

This is implemented by setting the "is prerendering" flag to true
whenever we suspend in the shell. But only if we haven't already skipped
over some siblings, because if so, then we need to schedule a separate
prerender pass regardless.
This commit is contained in:
Andrew Clark
2024-09-10 13:23:32 -04:00
committed by GitHub
parent d724ba96ff
commit 66cf2cfc8a
20 changed files with 96 additions and 521 deletions
@@ -238,12 +238,14 @@ describe('ReactCache', () => {
await act(() => jest.advanceTimersByTime(100));
assertLog([
'Promise resolved [4]',
1,
4,
'Suspend! [5]',
1,
4,
'Suspend! [5]',
'Promise resolved [5]',
1,
4,
@@ -274,12 +276,14 @@ describe('ReactCache', () => {
await act(() => jest.advanceTimersByTime(100));
assertLog([
'Promise resolved [2]',
1,
2,
'Suspend! [3]',
1,
2,
'Suspend! [3]',
'Promise resolved [3]',
1,
2,
@@ -744,7 +744,7 @@ describe('ReactDOMFiberAsync', () => {
// Because it suspended, it remains on the current path
expect(div.textContent).toBe('/path/a');
});
assertLog(gate('enableSiblingPrerendering') ? ['Suspend! [/path/b]'] : []);
assertLog([]);
await act(async () => {
resolvePromise();
+5 -44
View File
@@ -699,15 +699,7 @@ describe('ReactDOMForm', () => {
// This should suspend because form actions are implicitly wrapped
// in startTransition.
await submit(formRef.current);
assertLog([
'Pending...',
'Suspend! [Updated]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [Updated]', 'Loading...']
: []),
]);
assertLog(['Pending...', 'Suspend! [Updated]', 'Loading...']);
expect(container.textContent).toBe('Pending...Initial');
await act(() => resolveText('Updated'));
@@ -744,15 +736,7 @@ describe('ReactDOMForm', () => {
// Update
await submit(formRef.current);
assertLog([
'Pending...',
'Suspend! [Count: 1]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [Count: 1]', 'Loading...']
: []),
]);
assertLog(['Pending...', 'Suspend! [Count: 1]', 'Loading...']);
expect(container.textContent).toBe('Pending...Count: 0');
await act(() => resolveText('Count: 1'));
@@ -761,15 +745,7 @@ describe('ReactDOMForm', () => {
// Update again
await submit(formRef.current);
assertLog([
'Pending...',
'Suspend! [Count: 2]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [Count: 2]', 'Loading...']
: []),
]);
assertLog(['Pending...', 'Suspend! [Count: 2]', 'Loading...']);
expect(container.textContent).toBe('Pending...Count: 1');
await act(() => resolveText('Count: 2'));
@@ -813,14 +789,7 @@ describe('ReactDOMForm', () => {
assertLog(['Async action started', 'Pending...']);
await act(() => resolveText('Wait'));
assertLog([
'Suspend! [Updated]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [Updated]', 'Loading...']
: []),
]);
assertLog(['Suspend! [Updated]', 'Loading...']);
expect(container.textContent).toBe('Pending...Initial');
await act(() => resolveText('Updated'));
@@ -1506,15 +1475,7 @@ describe('ReactDOMForm', () => {
// Now dispatch inside of a transition. This one does not trigger a
// loading state.
await act(() => startTransition(() => dispatch()));
assertLog([
'Count: 1',
'Suspend! [Count: 2]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [Count: 2]', 'Loading...']
: []),
]);
assertLog(['Count: 1', 'Suspend! [Count: 2]', 'Loading...']);
expect(container.textContent).toBe('Count: 1');
await act(() => resolveText('Count: 2'));
+2 -2
View File
@@ -1690,7 +1690,7 @@ function mountSyncExternalStore<T>(
}
const rootRenderLanes = getWorkInProgressRootRenderLanes();
if (!includesBlockingLane(root, rootRenderLanes)) {
if (!includesBlockingLane(rootRenderLanes)) {
pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);
}
}
@@ -1802,7 +1802,7 @@ function updateSyncExternalStore<T>(
);
}
if (!isHydrating && !includesBlockingLane(root, renderLanes)) {
if (!isHydrating && !includesBlockingLane(renderLanes)) {
pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);
}
}
+1 -1
View File
@@ -579,7 +579,7 @@ export function includesOnlyTransitions(lanes: Lanes): boolean {
return (lanes & TransitionLanes) === lanes;
}
export function includesBlockingLane(root: FiberRoot, lanes: Lanes): boolean {
export function includesBlockingLane(lanes: Lanes): boolean {
const SyncDefaultLanes =
InputContinuousHydrationLane |
InputContinuousLane |
+13 -1
View File
@@ -907,7 +907,7 @@ export function performConcurrentWorkOnRoot(
// bug we're still investigating. Once the bug in Scheduler is fixed,
// we can remove this, since we track expiration ourselves.
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesBlockingLane(lanes) &&
!includesExpiredLane(root, lanes) &&
(disableSchedulerTimeoutInWorkLoop || !didTimeout);
let exitStatus = shouldTimeSlice
@@ -1968,6 +1968,18 @@ export function renderDidSuspend(): void {
export function renderDidSuspendDelayIfPossible(): void {
workInProgressRootExitStatus = RootSuspendedWithDelay;
if (
!workInProgressRootDidSkipSuspendedSiblings &&
!includesBlockingLane(workInProgressRootRenderLanes)
) {
// This render may not have originally been scheduled as a prerender, but
// something suspended inside the visible part of the tree, which means we
// won't be able to commit a fallback anyway. Let's proceed as if this were
// a prerender so that we can warm up the siblings without scheduling a
// separate pass.
workInProgressRootIsPrerendering = true;
}
// Check if there are updates that we skipped tree that might have unblocked
// this render.
if (
@@ -215,15 +215,7 @@ describe('Activity Suspense', () => {
);
});
});
assertLog([
'Open',
'Suspend! [Async]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Open', 'Suspend! [Async]', 'Loading...']
: []),
]);
assertLog(['Open', 'Suspend! [Async]', 'Loading...']);
// It should suspend with delay to prevent the already-visible Suspense
// boundary from switching to a fallback
expect(root).toMatchRenderedOutput(<span>Closed</span>);
@@ -284,15 +276,7 @@ describe('Activity Suspense', () => {
);
});
});
assertLog([
'Open',
'Suspend! [Async]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Open', 'Suspend! [Async]', 'Loading...']
: []),
]);
assertLog(['Open', 'Suspend! [Async]', 'Loading...']);
// It should suspend with delay to prevent the already-visible Suspense
// boundary from switching to a fallback
expect(root).toMatchRenderedOutput(
@@ -349,14 +349,7 @@ describe('act warnings', () => {
root.render(<App showMore={true} />);
});
});
assertLog([
'Suspend! [Async]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [Async]', 'Loading...']
: []),
]);
assertLog(['Suspend! [Async]', 'Loading...']);
expect(root).toMatchRenderedOutput('(empty)');
// This is a ping, not a retry, because no fallback is showing.
@@ -303,7 +303,7 @@ describe('ReactAsyncActions', () => {
'Suspend! [A1]',
...(gate('enableSiblingPrerendering')
? ['Pending: false', 'Suspend! [A1]', 'Suspend! [B1]', 'Suspend! [C1]']
? ['Suspend! [B1]', 'Suspend! [C1]']
: []),
]);
expect(root).toMatchRenderedOutput(
@@ -322,9 +322,7 @@ describe('ReactAsyncActions', () => {
'A1',
'Suspend! [B1]',
...(gate('enableSiblingPrerendering')
? ['Pending: false', 'A1', 'Suspend! [B1]', 'Suspend! [C1]']
: []),
...(gate('enableSiblingPrerendering') ? ['Suspend! [C1]'] : []),
]);
expect(root).toMatchRenderedOutput(
<>
@@ -333,16 +331,7 @@ describe('ReactAsyncActions', () => {
</>,
);
await act(() => resolveText('B1'));
assertLog([
'Pending: false',
'A1',
'B1',
'Suspend! [C1]',
...(gate('enableSiblingPrerendering')
? ['Pending: false', 'A1', 'B1', 'Suspend! [C1]']
: []),
]);
assertLog(['Pending: false', 'A1', 'B1', 'Suspend! [C1]']);
expect(root).toMatchRenderedOutput(
<>
<span>Pending: true</span>
@@ -715,10 +704,6 @@ describe('ReactAsyncActions', () => {
// automatically reverted.
'Pending: false',
'Suspend! [B]',
...(gate('enableSiblingPrerendering')
? ['Pending: false', 'Suspend! [B]']
: []),
]);
// Resolve the transition
@@ -209,16 +209,7 @@ describe('ReactConcurrentErrorRecovery', () => {
root.render(<App step={2} />);
});
});
assertLog([
'Suspend! [A2]',
'Loading...',
'Suspend! [B2]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [A2]', 'Loading...', 'Suspend! [B2]', 'Loading...']
: []),
]);
assertLog(['Suspend! [A2]', 'Loading...', 'Suspend! [B2]', 'Loading...']);
// Because this is a refresh, we don't switch to a fallback
expect(root).toMatchRenderedOutput('A1B1');
@@ -229,16 +220,7 @@ describe('ReactConcurrentErrorRecovery', () => {
// Because we're still suspended on A, we can't show an error boundary. We
// should wait for A to resolve.
assertLog([
'Suspend! [A2]',
'Loading...',
'Error! [B2]',
'Oops!',
...(gate('enableSiblingPrerendering')
? ['Suspend! [A2]', 'Loading...', 'Error! [B2]', 'Oops!']
: []),
]);
assertLog(['Suspend! [A2]', 'Loading...', 'Error! [B2]', 'Oops!']);
// Remain on previous screen.
expect(root).toMatchRenderedOutput('A1B1');
@@ -299,16 +281,7 @@ describe('ReactConcurrentErrorRecovery', () => {
root.render(<App step={2} />);
});
});
assertLog([
'Suspend! [A2]',
'Loading...',
'Suspend! [B2]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [A2]', 'Loading...', 'Suspend! [B2]', 'Loading...']
: []),
]);
assertLog(['Suspend! [A2]', 'Loading...', 'Suspend! [B2]', 'Loading...']);
// Because this is a refresh, we don't switch to a fallback
expect(root).toMatchRenderedOutput('A1B1');
@@ -364,11 +337,7 @@ describe('ReactConcurrentErrorRecovery', () => {
root.render(<AsyncText text="Async" />);
});
});
assertLog([
'Suspend! [Async]',
...(gate('enableSiblingPrerendering') ? ['Suspend! [Async]'] : []),
]);
assertLog(['Suspend! [Async]']);
expect(root).toMatchRenderedOutput(null);
// This also works if the suspended component is wrapped with an error
@@ -384,11 +353,7 @@ describe('ReactConcurrentErrorRecovery', () => {
);
});
});
assertLog([
'Suspend! [Async]',
...(gate('enableSiblingPrerendering') ? ['Suspend! [Async]'] : []),
]);
assertLog(['Suspend! [Async]']);
expect(root).toMatchRenderedOutput(null);
// Continues rendering once data resolves
@@ -445,7 +410,7 @@ describe('ReactConcurrentErrorRecovery', () => {
'Suspend! [Async]',
...(gate('enableSiblingPrerendering')
? ['Suspend! [Async]', 'Caught an error: Oops!']
? ['Caught an error: Oops!']
: []),
]);
// The render suspended without committing the error.
@@ -468,7 +433,7 @@ describe('ReactConcurrentErrorRecovery', () => {
'Suspend! [Async]',
...(gate('enableSiblingPrerendering')
? ['Suspend! [Async]', 'Caught an error: Oops!']
? ['Caught an error: Oops!']
: []),
]);
expect(root).toMatchRenderedOutput(null);
@@ -420,8 +420,6 @@ describe('ReactDeferredValue', () => {
// The initial value suspended, so we attempt the final value, which
// also suspends.
'Suspend! [Final]',
...(gate('enableSiblingPrerendering') ? ['Suspend! [Final]'] : []),
]);
expect(root).toMatchRenderedOutput(null);
@@ -461,8 +459,6 @@ describe('ReactDeferredValue', () => {
// The initial value suspended, so we attempt the final value, which
// also suspends.
'Suspend! [Final]',
...(gate('enableSiblingPrerendering') ? ['Suspend! [Final]'] : []),
]);
expect(root).toMatchRenderedOutput(null);
@@ -535,8 +531,6 @@ describe('ReactDeferredValue', () => {
// The initial value suspended, so we attempt the final value, which
// also suspends.
'Suspend! [Final]',
...(gate('enableSiblingPrerendering') ? ['Suspend! [Final]'] : []),
]);
expect(root).toMatchRenderedOutput(null);
@@ -546,8 +540,6 @@ describe('ReactDeferredValue', () => {
'Loading...',
// Still waiting for the final value.
'Suspend! [Final]',
...(gate('enableSiblingPrerendering') ? ['Suspend! [Final]'] : []),
]);
expect(root).toMatchRenderedOutput('Loading...');
@@ -592,8 +584,6 @@ describe('ReactDeferredValue', () => {
// boundaries work, where we always prefer to show the innermost
// loading state.)
'Suspend! [Content]',
...(gate('enableSiblingPrerendering') ? ['Suspend! [Content]'] : []),
]);
// Still showing the App preview state because the inner
// content suspended.
@@ -654,11 +654,10 @@ describe('ReactExpiration', () => {
});
await waitForAll([
'Suspend! [A1]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [A1]', 'B', 'C', 'Loading...']
: []),
...(gate('enableSiblingPrerendering') ? ['B', 'C'] : []),
'Loading...',
]);
// Lots of time elapses before the promise resolves
@@ -652,22 +652,14 @@ describe('ReactHooksWithNoopRenderer', () => {
React.startTransition(() => {
root.render(<Foo signal={false} />);
});
await waitForAll([
'Suspend!',
...(gate('enableSiblingPrerendering') ? ['Suspend!'] : []),
]);
await waitForAll(['Suspend!']);
expect(root).toMatchRenderedOutput(<span prop={0} />);
// Rendering again should suspend again.
React.startTransition(() => {
root.render(<Foo signal={false} />);
});
await waitForAll([
'Suspend!',
...(gate('enableSiblingPrerendering') ? ['Suspend!'] : []),
]);
await waitForAll(['Suspend!']);
});
it('discards render phase updates if something suspends, but not other updates in the same component', async () => {
@@ -717,22 +709,14 @@ describe('ReactHooksWithNoopRenderer', () => {
setLabel('B');
});
await waitForAll([
'Suspend!',
...(gate('enableSiblingPrerendering') ? ['Suspend!'] : []),
]);
await waitForAll(['Suspend!']);
expect(root).toMatchRenderedOutput(<span prop="A:0" />);
// Rendering again should suspend again.
React.startTransition(() => {
root.render(<Foo signal={false} />);
});
await waitForAll([
'Suspend!',
...(gate('enableSiblingPrerendering') ? ['Suspend!'] : []),
]);
await waitForAll(['Suspend!']);
// Flip the signal back to "cancel" the update. However, the update to
// label should still proceed. It shouldn't have been dropped.
@@ -3511,13 +3495,6 @@ describe('ReactHooksWithNoopRenderer', () => {
'Before... Pending: true',
'Suspend! [After... Pending: false]',
'Loading... Pending: false',
...(gate('enableSiblingPrerendering')
? [
'Suspend! [After... Pending: false]',
'Loading... Pending: false',
]
: []),
]);
expect(ReactNoop).toMatchRenderedOutput(
<span prop="Before... Pending: true" />,
@@ -3586,17 +3563,7 @@ describe('ReactHooksWithNoopRenderer', () => {
await act(async () => {
_setText('B');
await waitForAll([
'B',
'A',
'B',
'Suspend! [B]',
'Loading',
...(gate('enableSiblingPrerendering')
? ['B', 'Suspend! [B]', 'Loading']
: []),
]);
await waitForAll(['B', 'A', 'B', 'Suspend! [B]', 'Loading']);
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(
<>
@@ -1488,20 +1488,10 @@ describe('ReactLazy', () => {
React.startTransition(() => {
root.update(<Parent swap={true} />);
});
await waitForAll([
'Init B2',
'Loading...',
...(gate('enableSiblingPrerendering') ? ['Loading...'] : []),
]);
await waitForAll(['Init B2', 'Loading...']);
await act(() => resolveFakeImport(ChildB2));
// We need to flush to trigger the second one to load.
assertLog([
'Init A2',
'Loading...',
...(gate('enableSiblingPrerendering') ? ['Loading...'] : []),
]);
assertLog(['Init A2', 'Loading...']);
await act(() => resolveFakeImport(ChildA2));
assertLog(['b', 'a', 'Did update: b', 'Did update: a']);
expect(root).toMatchRenderedOutput('ba');
@@ -135,11 +135,10 @@ describe('ReactSuspense', () => {
'Bar',
// A suspends
'Suspend! [A]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Foo', 'Bar', 'Suspend! [A]', 'B', 'Loading...']
: []),
...(gate('enableSiblingPrerendering') ? ['B'] : []),
'Loading...',
]);
expect(container.textContent).toEqual('');
@@ -295,22 +295,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
'Bar',
// A suspends
'Suspend! [A]',
...(gate('enableSiblingPrerendering') ? ['B'] : []),
// We immediately unwind and switch to a fallback without
// rendering siblings.
'Loading...',
...(gate('enableSiblingPrerendering')
? [
'Foo',
'Bar',
// A suspends
'Suspend! [A]',
'B',
// We immediately unwind and switch to a fallback without
// rendering siblings.
'Loading...',
]
: []),
]);
expect(ReactNoop).toMatchRenderedOutput(null);
@@ -395,11 +385,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await waitForAll([
'A',
'Suspend! [B]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['A', 'Suspend! [B]', 'C', 'D', 'Loading...']
: []),
...(gate('enableSiblingPrerendering') ? ['C', 'D'] : []),
'Loading...',
]);
// Did not commit yet.
expect(ReactNoop).toMatchRenderedOutput(null);
@@ -457,14 +446,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
React.startTransition(() => {
ReactNoop.render(<App renderContent={true} />);
});
await waitForAll([
'Suspend! [Result]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [Result]', 'Loading...']
: []),
]);
await waitForAll(['Suspend! [Result]', 'Loading...']);
expect(ReactNoop).toMatchRenderedOutput(null);
await rejectText('Result', new Error('Failed to load: Result'));
@@ -579,14 +561,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Update the low-pri text
await act(() => startTransition(() => setLowPri('2')));
// Suspends
assertLog([
'Suspend! [2]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [2]', 'Loading...']
: []),
]);
assertLog(['Suspend! [2]', 'Loading...']);
// While we're still waiting for the low-pri update to complete, update the
// high-pri text at high priority.
@@ -631,14 +606,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
React.startTransition(() => {
ReactNoop.render(<App showA={true} showB={false} />);
});
await waitForAll([
'Suspend! [A]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [A]', 'Loading...']
: []),
]);
await waitForAll(['Suspend! [A]', 'Loading...']);
expect(ReactNoop).toMatchRenderedOutput(null);
React.startTransition(() => {
@@ -646,11 +614,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
await waitForAll([
'Suspend! [A]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [A]', 'B', 'Loading...']
: []),
...(gate('enableSiblingPrerendering') ? ['B'] : []),
'Loading...',
]);
expect(ReactNoop).toMatchRenderedOutput(null);
@@ -989,14 +956,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
</Suspense>,
);
});
await waitForAll([
'Suspend! [Async]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [Async]', 'Loading...']
: []),
]);
await waitForAll(['Suspend! [Async]', 'Loading...']);
expect(ReactNoop).toMatchRenderedOutput(null);
// Resolve the promise
@@ -1028,14 +988,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Schedule an update, and suspend for up to 5 seconds.
React.startTransition(() => ReactNoop.render(<App text="A" />));
// The update should suspend.
await waitForAll([
'Suspend! [A]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [A]', 'Loading...']
: []),
]);
await waitForAll(['Suspend! [A]', 'Loading...']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="S" />);
// Advance time until right before it expires.
@@ -1047,14 +1000,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Schedule another low priority update.
React.startTransition(() => ReactNoop.render(<App text="B" />));
// This update should also suspend.
await waitForAll([
'Suspend! [B]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [B]', 'Loading...']
: []),
]);
await waitForAll(['Suspend! [B]', 'Loading...']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="S" />);
// Schedule a regular update. Its expiration time will fall between
@@ -2320,16 +2266,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.render(<Foo showB={true} />);
});
await waitForAll([
'Foo',
'A',
'Suspend! [B]',
'Loading B...',
...(gate('enableSiblingPrerendering')
? ['Foo', 'A', 'Suspend! [B]', 'Loading B...']
: []),
]);
await waitForAll(['Foo', 'A', 'Suspend! [B]', 'Loading B...']);
// Transitions never fall back.
expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
@@ -2405,14 +2342,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Start transition.
React.startTransition(() => ReactNoop.render(<App page="B" />));
await waitForAll([
'Suspend! [B]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [B]', 'Loading...']
: []),
]);
await waitForAll(['Suspend! [B]', 'Loading...']);
Scheduler.unstable_advanceTime(100000);
await advanceTimers(100000);
// Even after lots of time has passed, we have still not yet flushed the
@@ -2463,14 +2393,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await act(async () => {
React.startTransition(() => transitionToPage('B'));
await waitForAll([
'Suspend! [B]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [B]', 'Loading...']
: []),
]);
await waitForAll(['Suspend! [B]', 'Loading...']);
Scheduler.unstable_advanceTime(100000);
await advanceTimers(100000);
// Even after lots of time has passed, we have still not yet flushed the
@@ -2525,14 +2448,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await act(async () => {
React.startTransition(() => transitionToPage('B'));
await waitForAll([
'Suspend! [B]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [B]', 'Loading...']
: []),
]);
await waitForAll(['Suspend! [B]', 'Loading...']);
Scheduler.unstable_advanceTime(100000);
await advanceTimers(100000);
// Even after lots of time has passed, we have still not yet flushed the
@@ -2574,14 +2490,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Start transition.
React.startTransition(() => ReactNoop.render(<App page="B" />));
await waitForAll([
'Suspend! [B]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [B]', 'Loading...']
: []),
]);
await waitForAll(['Suspend! [B]', 'Loading...']);
Scheduler.unstable_advanceTime(2999);
await advanceTimers(2999);
// Since the timeout is infinite (or effectively infinite),
@@ -2595,14 +2504,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Start a long (infinite) transition.
React.startTransition(() => ReactNoop.render(<App page="C" />));
await waitForAll([
'Suspend! [C]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [C]', 'Loading...']
: []),
]);
await waitForAll(['Suspend! [C]', 'Loading...']);
// Even after lots of time has passed, we have still not yet flushed the
// loading state.
@@ -2650,14 +2552,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await act(async () => {
React.startTransition(() => transitionToPage('B'));
await waitForAll([
'Suspend! [B]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [B]', 'Loading...']
: []),
]);
await waitForAll(['Suspend! [B]', 'Loading...']);
Scheduler.unstable_advanceTime(2999);
await advanceTimers(2999);
@@ -2675,14 +2570,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await act(async () => {
React.startTransition(() => transitionToPage('C'));
await waitForAll([
'Suspend! [C]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [C]', 'Loading...']
: []),
]);
await waitForAll(['Suspend! [C]', 'Loading...']);
// Even after lots of time has passed, we have still not yet flushed the
// loading state.
@@ -2734,14 +2622,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await act(async () => {
React.startTransition(() => transitionToPage('B'));
await waitForAll([
'Suspend! [B]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [B]', 'Loading...']
: []),
]);
await waitForAll(['Suspend! [B]', 'Loading...']);
Scheduler.unstable_advanceTime(2999);
await advanceTimers(2999);
// Since the timeout is infinite (or effectively infinite),
@@ -2758,14 +2639,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await act(async () => {
React.startTransition(() => transitionToPage('C'));
await waitForAll([
'Suspend! [C]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [C]', 'Loading...']
: []),
]);
await waitForAll(['Suspend! [C]', 'Loading...']);
// Even after lots of time has passed, we have still not yet flushed the
// loading state.
@@ -2806,15 +2680,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Start transition.
React.startTransition(() => ReactNoop.render(<App page="B" />));
await waitForAll([
'Hi!',
'Suspend! [B]',
'Loading B...',
...(gate('enableSiblingPrerendering')
? ['Hi!', 'Suspend! [B]', 'Loading B...']
: []),
]);
await waitForAll(['Hi!', 'Suspend! [B]', 'Loading B...']);
// Suspended
expect(ReactNoop).toMatchRenderedOutput(
@@ -2875,15 +2741,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Start transition.
React.startTransition(() => ReactNoop.render(<App page="B" />));
await waitForAll([
'Hi!',
'Suspend! [B]',
'Loading B...',
...(gate('enableSiblingPrerendering')
? ['Hi!', 'Suspend! [B]', 'Loading B...']
: []),
]);
await waitForAll(['Hi!', 'Suspend! [B]', 'Loading B...']);
// Suspended
expect(ReactNoop).toMatchRenderedOutput(
@@ -2997,14 +2855,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.render(<App showContent={true} />);
});
await waitForAll([
'Suspend! [A]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [A]', 'Loading...']
: []),
]);
await waitForAll(['Suspend! [A]', 'Loading...']);
await resolveText('A');
await waitFor(['A', 'Commit']);
expect(ReactNoop).toMatchRenderedOutput(
@@ -3056,14 +2907,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
ReactNoop.render(<App showContent={true} />);
});
await waitForAll([
'Suspend! [A]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [A]', 'Loading...']
: []),
]);
await waitForAll(['Suspend! [A]', 'Loading...']);
await resolveText('A');
await waitFor(['A', 'Commit']);
expect(ReactNoop).toMatchRenderedOutput(
@@ -3842,16 +3686,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// Update to "a". That will suspend.
await act(async () => {
setTextWithShortTransition('a');
await waitForAll([
'Pending...',
'',
'Suspend! [a]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [a]', 'Loading...']
: []),
]);
await waitForAll(['Pending...', '', 'Suspend! [a]', 'Loading...']);
});
assertLog([]);
expect(root).toMatchRenderedOutput(
@@ -3870,10 +3705,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
'',
'Suspend! [b]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [b]', 'Loading...']
: []),
]);
});
assertLog([]);
@@ -3888,14 +3719,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
await act(async () => {
await resolveText('a');
await waitForAll([
'Suspend! [b]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [b]', 'Loading...']
: []),
]);
await waitForAll(['Suspend! [b]', 'Loading...']);
expect(root).toMatchRenderedOutput(
<>
<span prop="Pending..." />
@@ -199,10 +199,6 @@ describe('ReactTransition', () => {
'(empty)',
'Suspend! [Async]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [Async]', 'Loading...']
: []),
]);
expect(root).toMatchRenderedOutput('Pending...(empty)');
@@ -273,10 +269,6 @@ describe('ReactTransition', () => {
'B label',
'Suspend! [B content]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['B label', 'Suspend! [B content]', 'Loading...']
: []),
]);
// This is a refresh transition so it shouldn't show a fallback
expect(root).toMatchRenderedOutput(
@@ -298,10 +290,6 @@ describe('ReactTransition', () => {
'C label',
'Suspend! [C content]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['C label', 'Suspend! [C content]', 'Loading...']
: []),
]);
expect(root).toMatchRenderedOutput(
<>
@@ -319,10 +307,6 @@ describe('ReactTransition', () => {
'C label',
'Suspend! [C content]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['C label', 'Suspend! [C content]', 'Loading...']
: []),
]);
expect(root).toMatchRenderedOutput(
<>
@@ -410,10 +394,6 @@ describe('ReactTransition', () => {
'B label',
'Suspend! [B content]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['B label', 'Suspend! [B content]', 'Loading...']
: []),
]);
// This is a refresh transition so it shouldn't show a fallback
expect(root).toMatchRenderedOutput(
@@ -435,10 +415,6 @@ describe('ReactTransition', () => {
'C label',
'Suspend! [C content]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['C label', 'Suspend! [C content]', 'Loading...']
: []),
]);
expect(root).toMatchRenderedOutput(
<>
@@ -456,10 +432,6 @@ describe('ReactTransition', () => {
'C label',
'Suspend! [C content]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['C label', 'Suspend! [C content]', 'Loading...']
: []),
]);
expect(root).toMatchRenderedOutput(
<>
@@ -528,14 +500,7 @@ describe('ReactTransition', () => {
setShowA(true);
});
});
assertLog([
'Suspend! [A]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [A]', 'Loading...']
: []),
]);
assertLog(['Suspend! [A]', 'Loading...']);
expect(root).toMatchRenderedOutput(null);
// Before A loads, switch to B. This should entangle A with B.
@@ -545,14 +510,7 @@ describe('ReactTransition', () => {
setShowB(true);
});
});
assertLog([
'Suspend! [B]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [B]', 'Loading...']
: []),
]);
assertLog(['Suspend! [B]', 'Loading...']);
expect(root).toMatchRenderedOutput(null);
// Before A or B loads, switch to C. This should entangle C with B, and
@@ -563,14 +521,7 @@ describe('ReactTransition', () => {
setShowC(true);
});
});
assertLog([
'Suspend! [C]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [C]', 'Loading...']
: []),
]);
assertLog(['Suspend! [C]', 'Loading...']);
expect(root).toMatchRenderedOutput(null);
// Now the data starts resolving out of order.
@@ -582,14 +533,7 @@ describe('ReactTransition', () => {
resolveText('B');
});
});
assertLog([
'Suspend! [C]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [C]', 'Loading...']
: []),
]);
assertLog(['Suspend! [C]', 'Loading...']);
expect(root).toMatchRenderedOutput(null);
// Now resolve A. Again, this will attempt to render C, since everything
@@ -599,14 +543,7 @@ describe('ReactTransition', () => {
resolveText('A');
});
});
assertLog([
'Suspend! [C]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [C]', 'Loading...']
: []),
]);
assertLog(['Suspend! [C]', 'Loading...']);
expect(root).toMatchRenderedOutput(null);
// Finally, resolve C. This time we can finish.
@@ -923,11 +860,10 @@ describe('ReactTransition', () => {
assertLog([
// Suspend.
'Suspend! [Async]',
'Loading...',
...(gate('enableSiblingPrerendering')
? ['Suspend! [Async]', 'Normal pri: 0', 'Loading...']
: []),
...(gate('enableSiblingPrerendering') ? ['Normal pri: 0'] : []),
'Loading...',
]);
expect(root).toMatchRenderedOutput('(empty), Normal pri: 0');
@@ -1885,10 +1885,6 @@ describe('ReactUse', () => {
'A component was suspended by an uncached promise. Creating ' +
'promises inside a Client Component or hook is not yet ' +
'supported, except via a Suspense-compatible library or framework.',
...(gate('enableSiblingPrerendering')
? ['A component was suspended by an uncached promise.']
: []),
]);
assertLog(['Async text requested [World]']);
@@ -1940,10 +1936,6 @@ describe('ReactUse', () => {
'A component was suspended by an uncached promise. Creating ' +
'promises inside a Client Component or hook is not yet ' +
'supported, except via a Suspense-compatible library or framework.',
...(gate('enableSiblingPrerendering')
? ['A component was suspended by an uncached promise.']
: []),
]);
assertLog(['Async text requested [World]']);
@@ -561,26 +561,13 @@ describe('useMemoCache()', () => {
root.render(<App chunkA={updatedChunkA} chunkB={updatedChunkB} />);
});
});
assertLog([
'Suspend! [chunkA]',
...(gate('enableSiblingPrerendering') ? ['Suspend! [chunkA]'] : []),
]);
assertLog(['Suspend! [chunkA]']);
// The data starts to stream in. Loading the data in the first chunk
// triggers an expensive computation in the UI. Later, we'll test whether
// this computation is reused.
await act(() => updatedChunkA.resolve('A2'));
assertLog([
'Some expensive processing... [A2]',
'Suspend! [chunkB]',
...(gate('enableSiblingPrerendering')
? gate('enableNoCloningMemoCache')
? ['Suspend! [chunkB]']
: ['Some expensive processing... [A2]', 'Suspend! [chunkB]']
: []),
]);
assertLog(['Some expensive processing... [A2]', 'Suspend! [chunkB]']);
// The second chunk hasn't loaded yet, so we're still showing the
// initial UI.
@@ -601,22 +588,11 @@ describe('useMemoCache()', () => {
if (gate(flags => flags.enableNoCloningMemoCache)) {
// We did not have process the first chunk again. We reused the
// computation from the earlier attempt.
assertLog([
'Suspend! [chunkB]',
...(gate('enableSiblingPrerendering') ? ['Suspend! [chunkB]'] : []),
]);
assertLog(['Suspend! [chunkB]']);
} else {
// Because we clone/reset the memo cache after every aborted attempt, we
// must process the first chunk again.
assertLog([
'Some expensive processing... [A2]',
'Suspend! [chunkB]',
...(gate('enableSiblingPrerendering')
? ['Some expensive processing... [A2]', 'Suspend! [chunkB]']
: []),
]);
assertLog(['Some expensive processing... [A2]', 'Suspend! [chunkB]']);
}
expect(root).toMatchRenderedOutput(
@@ -275,9 +275,7 @@ describe('useSyncExternalStore', () => {
// this test, this causes A to suspend.
'Suspend A',
...(gate('enableSiblingPrerendering')
? ['Suspend A', 'B: Updated']
: []),
...(gate('enableSiblingPrerendering') ? ['B: Updated'] : []),
]);
// Nothing has committed, because A suspended and no fallback
// was provided.