Skip to content

Commit b345523

Browse files
authored
[Fizz] Support abort reasons (#24680)
* [Fizz] Support abort reasons Fizz supports aborting the render but does not currently accept a reason. The various render functions that use Fizz have some automatic and some user-controlled abort semantics that can be useful to communicate with the running program and users about why an Abort happened. This change implements abort reasons for renderToReadableStream and renderToPipeable stream as well as legacy renderers such as renderToString and related implementations. For AbortController implementations the reason passed to the abort method is forwarded to Fizz and sent to the onError handler. If no reason is provided the AbortController should construct an AbortError DOMException and as a fallback Fizz will generate a similar error in the absence of a reason For pipeable streams, an abort function is returned alongside pipe which already accepted a reason. That reason is now forwarded to Fizz and the implementation described above. For legacy renderers there is no exposed abort functionality but it is used internally and the reasons provided give useful context to, for instance to the fact that Suspense is not supported in renderToString-like renderers
1 parent 79f54c1 commit b345523

17 files changed

+950
-422
lines changed

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

+179-1
Original file line numberDiff line numberDiff line change
@@ -1106,7 +1106,13 @@ describe('ReactDOMFizzServer', () => {
11061106
expect(Scheduler).toFlushAndYield([]);
11071107
expectErrors(
11081108
errors,
1109-
[['This Suspense boundary was aborted by the server.', expectedDigest]],
1109+
[
1110+
[
1111+
'The server did not finish this Suspense boundary: The render was aborted by the server without a reason.',
1112+
expectedDigest,
1113+
componentStack(['h1', 'Suspense', 'div', 'App']),
1114+
],
1115+
],
11101116
[
11111117
[
11121118
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
@@ -3057,6 +3063,178 @@ describe('ReactDOMFizzServer', () => {
30573063
);
30583064
});
30593065

3066+
// @gate experimental
3067+
it('Supports custom abort reasons with a string', async () => {
3068+
function App() {
3069+
return (
3070+
<div>
3071+
<p>
3072+
<Suspense fallback={'p'}>
3073+
<AsyncText text={'hello'} />
3074+
</Suspense>
3075+
</p>
3076+
<span>
3077+
<Suspense fallback={'span'}>
3078+
<AsyncText text={'world'} />
3079+
</Suspense>
3080+
</span>
3081+
</div>
3082+
);
3083+
}
3084+
3085+
let abort;
3086+
const loggedErrors = [];
3087+
await act(async () => {
3088+
const {
3089+
pipe,
3090+
abort: abortImpl,
3091+
} = ReactDOMFizzServer.renderToPipeableStream(<App />, {
3092+
onError(error) {
3093+
// In this test we contrive erroring with strings so we push the error whereas in most
3094+
// other tests we contrive erroring with Errors and push the message.
3095+
loggedErrors.push(error);
3096+
return 'a digest';
3097+
},
3098+
});
3099+
abort = abortImpl;
3100+
pipe(writable);
3101+
});
3102+
3103+
expect(loggedErrors).toEqual([]);
3104+
expect(getVisibleChildren(container)).toEqual(
3105+
<div>
3106+
<p>p</p>
3107+
<span>span</span>
3108+
</div>,
3109+
);
3110+
3111+
await act(() => {
3112+
abort('foobar');
3113+
});
3114+
3115+
expect(loggedErrors).toEqual(['foobar', 'foobar']);
3116+
3117+
const errors = [];
3118+
ReactDOMClient.hydrateRoot(container, <App />, {
3119+
onRecoverableError(error, errorInfo) {
3120+
errors.push({error, errorInfo});
3121+
},
3122+
});
3123+
3124+
expect(Scheduler).toFlushAndYield([]);
3125+
3126+
expectErrors(
3127+
errors,
3128+
[
3129+
[
3130+
'The server did not finish this Suspense boundary: foobar',
3131+
'a digest',
3132+
componentStack(['Suspense', 'p', 'div', 'App']),
3133+
],
3134+
[
3135+
'The server did not finish this Suspense boundary: foobar',
3136+
'a digest',
3137+
componentStack(['Suspense', 'span', 'div', 'App']),
3138+
],
3139+
],
3140+
[
3141+
[
3142+
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
3143+
'a digest',
3144+
],
3145+
[
3146+
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
3147+
'a digest',
3148+
],
3149+
],
3150+
);
3151+
});
3152+
3153+
// @gate experimental
3154+
it('Supports custom abort reasons with an Error', async () => {
3155+
function App() {
3156+
return (
3157+
<div>
3158+
<p>
3159+
<Suspense fallback={'p'}>
3160+
<AsyncText text={'hello'} />
3161+
</Suspense>
3162+
</p>
3163+
<span>
3164+
<Suspense fallback={'span'}>
3165+
<AsyncText text={'world'} />
3166+
</Suspense>
3167+
</span>
3168+
</div>
3169+
);
3170+
}
3171+
3172+
let abort;
3173+
const loggedErrors = [];
3174+
await act(async () => {
3175+
const {
3176+
pipe,
3177+
abort: abortImpl,
3178+
} = ReactDOMFizzServer.renderToPipeableStream(<App />, {
3179+
onError(error) {
3180+
loggedErrors.push(error.message);
3181+
return 'a digest';
3182+
},
3183+
});
3184+
abort = abortImpl;
3185+
pipe(writable);
3186+
});
3187+
3188+
expect(loggedErrors).toEqual([]);
3189+
expect(getVisibleChildren(container)).toEqual(
3190+
<div>
3191+
<p>p</p>
3192+
<span>span</span>
3193+
</div>,
3194+
);
3195+
3196+
await act(() => {
3197+
abort(new Error('uh oh'));
3198+
});
3199+
3200+
expect(loggedErrors).toEqual(['uh oh', 'uh oh']);
3201+
3202+
const errors = [];
3203+
ReactDOMClient.hydrateRoot(container, <App />, {
3204+
onRecoverableError(error, errorInfo) {
3205+
errors.push({error, errorInfo});
3206+
},
3207+
});
3208+
3209+
expect(Scheduler).toFlushAndYield([]);
3210+
3211+
expectErrors(
3212+
errors,
3213+
[
3214+
[
3215+
'The server did not finish this Suspense boundary: uh oh',
3216+
'a digest',
3217+
componentStack(['Suspense', 'p', 'div', 'App']),
3218+
],
3219+
[
3220+
'The server did not finish this Suspense boundary: uh oh',
3221+
'a digest',
3222+
componentStack(['Suspense', 'span', 'div', 'App']),
3223+
],
3224+
],
3225+
[
3226+
[
3227+
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
3228+
'a digest',
3229+
],
3230+
[
3231+
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
3232+
'a digest',
3233+
],
3234+
],
3235+
);
3236+
});
3237+
30603238
describe('error escaping', () => {
30613239
//@gate experimental
30623240
it('escapes error hash, message, and component stack values in directly flushed errors (html escaping)', async () => {

0 commit comments

Comments
 (0)