Skip to content

Commit 6b31297

Browse files
committed
Fizz implementation
This is semantically the same as just throwing an error. It just triggers client rendering. The difference is in how it gets logged. On the server we log to onPostpone instead of onError. On the client this doesn't trigger a recoverable error. It's just silent since it was intentional.
1 parent eec516e commit 6b31297

13 files changed

+210
-16
lines changed

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

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6040,4 +6040,95 @@ describe('ReactDOMFizzServer', () => {
60406040
console.error = originalConsoleError;
60416041
}
60426042
});
6043+
6044+
// @gate enablePostpone
6045+
it('client renders postponed boundaries without erroring', async () => {
6046+
function Postponed({isClient}) {
6047+
if (!isClient) {
6048+
React.unstable_postpone('testing postpone');
6049+
}
6050+
return 'client only';
6051+
}
6052+
6053+
function App({isClient}) {
6054+
return (
6055+
<div>
6056+
<Suspense fallback={'loading...'}>
6057+
<Postponed isClient={isClient} />
6058+
</Suspense>
6059+
</div>
6060+
);
6061+
}
6062+
6063+
const errors = [];
6064+
6065+
await act(() => {
6066+
const {pipe} = renderToPipeableStream(<App isClient={false} />, {
6067+
onError(error) {
6068+
errors.push(error.message);
6069+
},
6070+
});
6071+
pipe(writable);
6072+
});
6073+
6074+
expect(getVisibleChildren(container)).toEqual(<div>loading...</div>);
6075+
6076+
ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
6077+
onRecoverableError(error) {
6078+
errors.push(error.message);
6079+
},
6080+
});
6081+
await waitForAll([]);
6082+
// Postponing should not be logged as a recoverable error since it's intentional.
6083+
expect(errors).toEqual([]);
6084+
expect(getVisibleChildren(container)).toEqual(<div>client only</div>);
6085+
});
6086+
6087+
// @gate enablePostpone
6088+
it('errors if trying to postpone outside a Suspense boundary', async () => {
6089+
function Postponed() {
6090+
React.unstable_postpone('testing postpone');
6091+
return 'client only';
6092+
}
6093+
6094+
function App() {
6095+
return (
6096+
<div>
6097+
<Postponed />
6098+
</div>
6099+
);
6100+
}
6101+
6102+
const errors = [];
6103+
const fatalErrors = [];
6104+
const postponed = [];
6105+
let written = false;
6106+
6107+
const testWritable = new Stream.Writable();
6108+
testWritable._write = (chunk, encoding, next) => {
6109+
written = true;
6110+
};
6111+
6112+
await act(() => {
6113+
const {pipe} = renderToPipeableStream(<App />, {
6114+
onPostpone(reason) {
6115+
postponed.push(reason);
6116+
},
6117+
onError(error) {
6118+
errors.push(error.message);
6119+
},
6120+
onShellError(error) {
6121+
fatalErrors.push(error.message);
6122+
},
6123+
});
6124+
pipe(testWritable);
6125+
});
6126+
6127+
expect(written).toBe(false);
6128+
// Postponing is not logged as an error but as a postponed reason.
6129+
expect(errors).toEqual([]);
6130+
expect(postponed).toEqual(['testing postpone']);
6131+
// However, it does error the shell.
6132+
expect(fatalErrors).toEqual(['testing postpone']);
6133+
});
60436134
});

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,4 +503,43 @@ describe('ReactDOMFizzServerBrowser', () => {
503503
`"<link rel="preload" href="init.js" as="script" fetchPriority="low" nonce="R4nd0m"/><link rel="modulepreload" href="init.mjs" fetchPriority="low" nonce="R4nd0m"/><div>hello world</div><script nonce="${nonce}">INIT();</script><script src="init.js" nonce="${nonce}" async=""></script><script type="module" src="init.mjs" nonce="${nonce}" async=""></script>"`,
504504
);
505505
});
506+
507+
// @gate enablePostpone
508+
it('errors if trying to postpone outside a Suspense boundary', async () => {
509+
function Postponed() {
510+
React.unstable_postpone('testing postpone');
511+
return 'client only';
512+
}
513+
514+
function App() {
515+
return (
516+
<div>
517+
<Postponed />
518+
</div>
519+
);
520+
}
521+
522+
const errors = [];
523+
const postponed = [];
524+
525+
let caughtError = null;
526+
try {
527+
await ReactDOMFizzServer.renderToReadableStream(<App />, {
528+
onError(error) {
529+
errors.push(error.message);
530+
},
531+
onPostpone(reason) {
532+
postponed.push(reason);
533+
},
534+
});
535+
} catch (error) {
536+
caughtError = error;
537+
}
538+
539+
// Postponing is not logged as an error but as a postponed reason.
540+
expect(errors).toEqual([]);
541+
expect(postponed).toEqual(['testing postpone']);
542+
// However, it does error the shell.
543+
expect(caughtError.message).toEqual('testing postpone');
544+
});
506545
});

packages/react-dom/src/server/ReactDOMFizzServerBrowser.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type Options = {
3535
progressiveChunkSize?: number,
3636
signal?: AbortSignal,
3737
onError?: (error: mixed) => ?string,
38+
onPostpone?: (reason: string) => void,
3839
unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
3940
};
4041

@@ -100,6 +101,7 @@ function renderToReadableStream(
100101
onShellReady,
101102
onShellError,
102103
onFatalError,
104+
options ? options.onPostpone : undefined,
103105
);
104106
if (options && options.signal) {
105107
const signal = options.signal;

packages/react-dom/src/server/ReactDOMFizzServerBun.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type Options = {
3535
progressiveChunkSize?: number,
3636
signal?: AbortSignal,
3737
onError?: (error: mixed) => ?string,
38+
onPostpone?: (reason: string) => void,
3839
unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
3940
};
4041

@@ -101,6 +102,7 @@ function renderToReadableStream(
101102
onShellReady,
102103
onShellError,
103104
onFatalError,
105+
options ? options.onPostpone : undefined,
104106
);
105107
if (options && options.signal) {
106108
const signal = options.signal;

packages/react-dom/src/server/ReactDOMFizzServerEdge.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type Options = {
3535
progressiveChunkSize?: number,
3636
signal?: AbortSignal,
3737
onError?: (error: mixed) => ?string,
38+
onPostpone?: (reason: string) => void,
3839
unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
3940
};
4041

@@ -100,6 +101,7 @@ function renderToReadableStream(
100101
onShellReady,
101102
onShellError,
102103
onFatalError,
104+
options ? options.onPostpone : undefined,
103105
);
104106
if (options && options.signal) {
105107
const signal = options.signal;

packages/react-dom/src/server/ReactDOMFizzServerNode.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ type Options = {
4949
onShellError?: (error: mixed) => void,
5050
onAllReady?: () => void,
5151
onError?: (error: mixed) => ?string,
52+
onPostpone?: (reason: string) => void,
5253
unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
5354
};
5455

@@ -80,6 +81,7 @@ function createRequestImpl(children: ReactNodeList, options: void | Options) {
8081
options ? options.onShellReady : undefined,
8182
options ? options.onShellError : undefined,
8283
undefined,
84+
options ? options.onPostpone : undefined,
8385
);
8486
}
8587

packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type Options = {
3434
progressiveChunkSize?: number,
3535
signal?: AbortSignal,
3636
onError?: (error: mixed) => ?string,
37+
onPostpone?: (reason: string) => void,
3738
unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
3839
};
3940

@@ -85,6 +86,7 @@ function prerender(
8586
undefined,
8687
undefined,
8788
onFatalError,
89+
options ? options.onPostpone : undefined,
8890
);
8991
if (options && options.signal) {
9092
const signal = options.signal;

packages/react-dom/src/server/ReactDOMFizzStaticEdge.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type Options = {
3434
progressiveChunkSize?: number,
3535
signal?: AbortSignal,
3636
onError?: (error: mixed) => ?string,
37+
onPostpone?: (reason: string) => void,
3738
unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
3839
};
3940

@@ -85,6 +86,7 @@ function prerender(
8586
undefined,
8687
undefined,
8788
onFatalError,
89+
options ? options.onPostpone : undefined,
8890
);
8991
if (options && options.signal) {
9092
const signal = options.signal;

packages/react-dom/src/server/ReactDOMFizzStaticNode.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type Options = {
3636
progressiveChunkSize?: number,
3737
signal?: AbortSignal,
3838
onError?: (error: mixed) => ?string,
39+
onPostpone?: (reason: string) => void,
3940
unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor,
4041
};
4142

@@ -99,6 +100,7 @@ function prerenderToNodeStreams(
99100
undefined,
100101
undefined,
101102
onFatalError,
103+
options ? options.onPostpone : undefined,
102104
);
103105
if (options && options.signal) {
104106
const signal = options.signal;

packages/react-dom/src/server/ReactDOMLegacyServerImpl.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ function renderToStringImpl(
7979
onShellReady,
8080
undefined,
8181
undefined,
82+
undefined,
8283
);
8384
startWork(request);
8485
// If anything suspended and is still pending, we'll abort it before writing.

0 commit comments

Comments
 (0)