Skip to content

Commit dfafc84

Browse files
committed
Track postpones in fallbacks
1 parent 430e712 commit dfafc84

File tree

3 files changed

+378
-45
lines changed

3 files changed

+378
-45
lines changed

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

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6261,6 +6261,59 @@ describe('ReactDOMFizzServer', () => {
62616261
expect(fatalErrors).toEqual(['testing postpone']);
62626262
});
62636263

6264+
// @gate enablePostpone
6265+
it('can postpone in a fallback', async () => {
6266+
function Postponed({isClient}) {
6267+
if (!isClient) {
6268+
React.unstable_postpone('testing postpone');
6269+
}
6270+
return 'loading...';
6271+
}
6272+
6273+
const lazyText = React.lazy(async () => {
6274+
await 0; // causes the fallback to start work
6275+
return {default: 'Hello'};
6276+
});
6277+
6278+
function App({isClient}) {
6279+
return (
6280+
<div>
6281+
<Suspense fallback="Outer">
6282+
<Suspense fallback={<Postponed isClient={isClient} />}>
6283+
{lazyText}
6284+
</Suspense>
6285+
</Suspense>
6286+
</div>
6287+
);
6288+
}
6289+
6290+
const errors = [];
6291+
6292+
await act(() => {
6293+
const {pipe} = renderToPipeableStream(<App isClient={false} />, {
6294+
onError(error) {
6295+
errors.push(error.message);
6296+
},
6297+
});
6298+
pipe(writable);
6299+
});
6300+
6301+
// TODO: This should actually be fully resolved because the value could eventually
6302+
// resolve on the server even though the fallback couldn't so we should have been
6303+
// able to render it.
6304+
expect(getVisibleChildren(container)).toEqual(<div>Outer</div>);
6305+
6306+
ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
6307+
onRecoverableError(error) {
6308+
errors.push(error.message);
6309+
},
6310+
});
6311+
await waitForAll([]);
6312+
// Postponing should not be logged as a recoverable error since it's intentional.
6313+
expect(errors).toEqual([]);
6314+
expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
6315+
});
6316+
62646317
it(
62656318
'a transition that flows into a dehydrated boundary should not suspend ' +
62666319
'if the boundary is showing a fallback',
@@ -6830,4 +6883,94 @@ describe('ReactDOMFizzServer', () => {
68306883
],
68316884
);
68326885
});
6886+
6887+
// @gate enablePostpone
6888+
it('can postpone in fallback', async () => {
6889+
let prerendering = true;
6890+
function Postpone() {
6891+
if (prerendering) {
6892+
React.unstable_postpone();
6893+
}
6894+
return 'Hello';
6895+
}
6896+
6897+
let resolve;
6898+
const promise = new Promise(r => (resolve = r));
6899+
6900+
function PostponeAndDelay() {
6901+
if (prerendering) {
6902+
React.unstable_postpone();
6903+
}
6904+
return React.use(promise);
6905+
}
6906+
6907+
const Lazy = React.lazy(async () => {
6908+
await 0;
6909+
return {default: Postpone};
6910+
});
6911+
6912+
function App() {
6913+
return (
6914+
<div>
6915+
<Suspense fallback="Outer">
6916+
<Suspense fallback={<Postpone />}>
6917+
<PostponeAndDelay /> World
6918+
</Suspense>
6919+
<Suspense fallback={<Postpone />}>
6920+
<Lazy />
6921+
</Suspense>
6922+
</Suspense>
6923+
</div>
6924+
);
6925+
}
6926+
6927+
const prerendered = await ReactDOMFizzStatic.prerenderToNodeStream(<App />);
6928+
expect(prerendered.postponed).not.toBe(null);
6929+
6930+
prerendering = false;
6931+
6932+
// Create a separate stream so it doesn't close the writable. I.e. simple concat.
6933+
const preludeWritable = new Stream.PassThrough();
6934+
preludeWritable.setEncoding('utf8');
6935+
preludeWritable.on('data', chunk => {
6936+
writable.write(chunk);
6937+
});
6938+
6939+
await act(() => {
6940+
prerendered.prelude.pipe(preludeWritable);
6941+
});
6942+
6943+
const resumed = await ReactDOMFizzServer.resumeToPipeableStream(
6944+
<App />,
6945+
JSON.parse(JSON.stringify(prerendered.postponed)),
6946+
);
6947+
6948+
expect(getVisibleChildren(container)).toEqual(<div>Outer</div>);
6949+
6950+
// Read what we've completed so far
6951+
await act(() => {
6952+
resumed.pipe(writable);
6953+
});
6954+
6955+
// Should have now resolved the postponed loading state, but not the promise
6956+
expect(getVisibleChildren(container)).toEqual(
6957+
<div>
6958+
{'Hello'}
6959+
{'Hello'}
6960+
</div>,
6961+
);
6962+
6963+
// Resolve the final promise
6964+
await act(() => {
6965+
resolve('Hi');
6966+
});
6967+
6968+
expect(getVisibleChildren(container)).toEqual(
6969+
<div>
6970+
{'Hi'}
6971+
{' World'}
6972+
{'Hello'}
6973+
</div>,
6974+
);
6975+
});
68336976
});

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

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -870,8 +870,6 @@ describe('ReactDOMFizzStaticBrowser', () => {
870870

871871
prerendering = false;
872872

873-
console.log(JSON.stringify(prerendered.postponed, null, 2));
874-
875873
const resumed = await ReactDOMFizzServer.resume(
876874
<App />,
877875
JSON.parse(JSON.stringify(prerendered.postponed)),
@@ -887,4 +885,100 @@ describe('ReactDOMFizzStaticBrowser', () => {
887885
<div>{['Hello', 'Hello', 'Hello']}</div>,
888886
);
889887
});
888+
889+
// @gate enablePostpone
890+
it('can postpone in fallback', async () => {
891+
let prerendering = true;
892+
function Postpone() {
893+
if (prerendering) {
894+
React.unstable_postpone();
895+
}
896+
return 'Hello';
897+
}
898+
899+
const Lazy = React.lazy(async () => {
900+
await 0;
901+
return {default: Postpone};
902+
});
903+
904+
function App() {
905+
return (
906+
<div>
907+
<Suspense fallback="Outer">
908+
<Suspense fallback={<Postpone />}>
909+
<Postpone /> World
910+
</Suspense>
911+
<Suspense fallback={<Postpone />}>
912+
<Lazy />
913+
</Suspense>
914+
</Suspense>
915+
</div>
916+
);
917+
}
918+
919+
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
920+
expect(prerendered.postponed).not.toBe(null);
921+
922+
prerendering = false;
923+
924+
const resumed = await ReactDOMFizzServer.resume(
925+
<App />,
926+
JSON.parse(JSON.stringify(prerendered.postponed)),
927+
);
928+
929+
await readIntoContainer(prerendered.prelude);
930+
931+
expect(getVisibleChildren(container)).toEqual(<div>Outer</div>);
932+
933+
await readIntoContainer(resumed);
934+
935+
expect(getVisibleChildren(container)).toEqual(
936+
<div>
937+
{'Hello'}
938+
{' World'}
939+
{'Hello'}
940+
</div>,
941+
);
942+
});
943+
944+
// @gate enablePostpone
945+
it('can postpone in fallback without postponing the tree', async () => {
946+
function Postpone() {
947+
React.unstable_postpone();
948+
}
949+
950+
const lazyText = React.lazy(async () => {
951+
await 0; // causes the fallback to start work
952+
return {default: 'Hello'};
953+
});
954+
955+
function App() {
956+
return (
957+
<div>
958+
<Suspense fallback="Outer">
959+
<Suspense fallback={<Postpone />}>{lazyText}</Suspense>
960+
</Suspense>
961+
</div>
962+
);
963+
}
964+
965+
const prerendered = await ReactDOMFizzStatic.prerender(<App />);
966+
// TODO: This should actually be null because we should've been able to fully
967+
// resolve the render on the server eventually, even though the fallback postponed.
968+
// So we should not need to resume.
969+
expect(prerendered.postponed).not.toBe(null);
970+
971+
await readIntoContainer(prerendered.prelude);
972+
973+
expect(getVisibleChildren(container)).toEqual(<div>Outer</div>);
974+
975+
const resumed = await ReactDOMFizzServer.resume(
976+
<App />,
977+
JSON.parse(JSON.stringify(prerendered.postponed)),
978+
);
979+
980+
await readIntoContainer(resumed);
981+
982+
expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
983+
});
890984
});

0 commit comments

Comments
 (0)