Skip to content

Commit 1b5ea1f

Browse files
committed
use return from onError
1 parent 645ec5d commit 1b5ea1f

File tree

9 files changed

+156
-46
lines changed

9 files changed

+156
-46
lines changed

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

Lines changed: 90 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,34 @@ describe('ReactDOMFizzServer', () => {
8989
});
9090
});
9191

92+
function expectErrors(errorsArr, toBeDevArr, toBeProdArr) {
93+
if (__DEV__) {
94+
expect(errorsArr.map(error => normalizeCodeLocInfo(error))).toEqual(
95+
toBeDevArr.map(error => {
96+
if (typeof error === 'string' || error instanceof String) {
97+
return error;
98+
}
99+
let str = JSON.stringify(error).replace(/\\n/g, '\n');
100+
// this gets stripped away by normalizeCodeLocInfo...
101+
// Kind of hacky but lets strip it away here too just so they match...
102+
// easier than fixing the regex to account for this edge case
103+
if (str.endsWith('at **)"}')) {
104+
str = str.replace(/at \*\*\)\"}$/, 'at **)');
105+
}
106+
return str;
107+
}),
108+
);
109+
} else {
110+
expect(errorsArr).toEqual(toBeProdArr);
111+
}
112+
}
113+
114+
function componentStack(components) {
115+
return components
116+
.map(component => `\n in ${component} (at **)`)
117+
.join('');
118+
}
119+
92120
async function act(callback) {
93121
await callback();
94122
// Await one turn around the event loop.
@@ -421,12 +449,13 @@ describe('ReactDOMFizzServer', () => {
421449
}
422450

423451
let bootstrapped = false;
452+
const errors = [];
424453
window.__INIT__ = function() {
425454
bootstrapped = true;
426455
// Attempt to hydrate the content.
427456
ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
428457
onRecoverableError(error) {
429-
Scheduler.unstable_yieldValue(error.message);
458+
errors.push(error.message);
430459
},
431460
});
432461
};
@@ -438,6 +467,7 @@ describe('ReactDOMFizzServer', () => {
438467
bootstrapScriptContent: '__INIT__();',
439468
onError(x) {
440469
loggedErrors.push(x);
470+
return 'Hash';
441471
},
442472
},
443473
);
@@ -464,10 +494,17 @@ describe('ReactDOMFizzServer', () => {
464494
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
465495

466496
// Now we can client render it instead.
467-
expect(Scheduler).toFlushAndYield([
468-
'The server could not finish this Suspense boundary, likely due to ' +
469-
'an error during server rendering. Switched to client rendering.',
470-
]);
497+
expect(Scheduler).toFlushAndYield([]);
498+
expectErrors(
499+
errors,
500+
[
501+
{
502+
error: theError.message,
503+
componentStack: componentStack(['Lazy', 'Suspense', 'div', 'App']),
504+
},
505+
],
506+
['Hash'],
507+
);
471508

472509
// The client rendered HTML is now in place.
473510
expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
@@ -534,17 +571,19 @@ describe('ReactDOMFizzServer', () => {
534571
{
535572
onError(x) {
536573
loggedErrors.push(x);
574+
return 'hash';
537575
},
538576
},
539577
);
540578
pipe(writable);
541579
});
542580
expect(loggedErrors).toEqual([]);
543581

582+
const errors = [];
544583
// Attempt to hydrate the content.
545584
ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
546585
onRecoverableError(error) {
547-
Scheduler.unstable_yieldValue(error.message);
586+
errors.push(error.message);
548587
},
549588
});
550589
Scheduler.unstable_flushAll();
@@ -565,10 +604,18 @@ describe('ReactDOMFizzServer', () => {
565604
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
566605

567606
// Now we can client render it instead.
568-
expect(Scheduler).toFlushAndYield([
569-
'The server could not finish this Suspense boundary, likely due to ' +
570-
'an error during server rendering. Switched to client rendering.',
571-
]);
607+
expect(Scheduler).toFlushAndYield([]);
608+
609+
expectErrors(
610+
errors,
611+
[
612+
{
613+
error: theError.message,
614+
componentStack: componentStack(['div', 'App']),
615+
},
616+
],
617+
['hash'],
618+
);
572619

573620
// The client rendered HTML is now in place.
574621
expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
@@ -852,10 +899,11 @@ describe('ReactDOMFizzServer', () => {
852899

853900
// We're still showing a fallback.
854901

902+
const errors = [];
855903
// Attempt to hydrate the content.
856904
ReactDOMClient.hydrateRoot(container, <App />, {
857905
onRecoverableError(error) {
858-
Scheduler.unstable_yieldValue(error.message);
906+
errors.push(error.message);
859907
},
860908
});
861909
Scheduler.unstable_flushAll();
@@ -869,10 +917,12 @@ describe('ReactDOMFizzServer', () => {
869917
});
870918

871919
// We still can't render it on the client.
872-
expect(Scheduler).toFlushAndYield([
873-
'The server could not finish this Suspense boundary, likely due to an ' +
874-
'error during server rendering. Switched to client rendering.',
875-
]);
920+
expect(Scheduler).toFlushAndYield([]);
921+
expectErrors(
922+
errors,
923+
['This Suspense boundary was aborted by the server'],
924+
['This Suspense boundary was aborted by the server'],
925+
);
876926
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
877927

878928
// We now resolve it on the client.
@@ -1540,6 +1590,7 @@ describe('ReactDOMFizzServer', () => {
15401590
{
15411591
onError(x) {
15421592
loggedErrors.push(x);
1593+
return 'error hash';
15431594
},
15441595
},
15451596
);
@@ -1548,10 +1599,11 @@ describe('ReactDOMFizzServer', () => {
15481599

15491600
// We're still showing a fallback.
15501601

1602+
const errors = [];
15511603
// Attempt to hydrate the content.
15521604
ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
15531605
onRecoverableError(error) {
1554-
Scheduler.unstable_yieldValue(error.message);
1606+
errors.push(error.message);
15551607
},
15561608
});
15571609
Scheduler.unstable_flushAll();
@@ -1582,10 +1634,24 @@ describe('ReactDOMFizzServer', () => {
15821634
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
15831635

15841636
// That will let us client render it instead.
1585-
expect(Scheduler).toFlushAndYield([
1586-
'The server could not finish this Suspense boundary, likely due to ' +
1587-
'an error during server rendering. Switched to client rendering.',
1588-
]);
1637+
expect(Scheduler).toFlushAndYield([]);
1638+
expectErrors(
1639+
errors,
1640+
[
1641+
{
1642+
error: theError.message,
1643+
componentStack: componentStack([
1644+
'AsyncText',
1645+
'h1',
1646+
'Suspense',
1647+
'div',
1648+
'Suspense',
1649+
'App',
1650+
]),
1651+
},
1652+
],
1653+
['error hash'],
1654+
);
15891655

15901656
// The client rendered HTML is now in place.
15911657
expect(getVisibleChildren(container)).toEqual(
@@ -2205,18 +2271,19 @@ describe('ReactDOMFizzServer', () => {
22052271

22062272
// Hydrate the tree. Child will throw during render.
22072273
isClient = true;
2274+
const errors = [];
22082275
ReactDOMClient.hydrateRoot(container, <App />, {
22092276
onRecoverableError(error) {
2210-
Scheduler.unstable_yieldValue(
2211-
'Log recoverable error: ' + error.message,
2212-
);
2277+
errors.push(error.message);
22132278
},
22142279
});
22152280

22162281
// Because we failed to recover from the error, onRecoverableError
22172282
// shouldn't be called.
22182283
expect(Scheduler).toFlushAndYield([]);
22192284
expect(getVisibleChildren(container)).toEqual('Oops!');
2285+
2286+
expectErrors(errors, [], []);
22202287
},
22212288
);
22222289

packages/react-dom/src/client/ReactDOMHostConfig.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,11 @@ export function isSuspenseInstancePending(instance: SuspenseInstance) {
734734
export function isSuspenseInstanceFallback(instance: SuspenseInstance) {
735735
return instance.data === SUSPENSE_FALLBACK_START_DATA;
736736
}
737+
export function getSuspenseInstanceFallbackError(
738+
instance: SuspenseInstance,
739+
): string {
740+
return (instance: any).data2;
741+
}
737742

738743
export function registerSuspenseInstanceRetry(
739744
instance: SuspenseInstance,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ type Options = {|
4343
onShellReady?: () => void,
4444
onShellError?: () => void,
4545
onAllReady?: () => void,
46-
onError?: (error: mixed) => void,
46+
onError?: (error: mixed) => string,
4747
|};
4848

4949
type Controls = {|

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1681,7 +1681,7 @@ export function writeEndSegment(
16811681
// const SUSPENSE_PENDING_START_DATA = '$?';
16821682
// const SUSPENSE_FALLBACK_START_DATA = '$!';
16831683
//
1684-
// function clientRenderBoundary(suspenseBoundaryID) {
1684+
// function clientRenderBoundary(suspenseBoundaryID, errorMsg) {
16851685
// // Find the fallback's first element.
16861686
// const suspenseIdNode = document.getElementById(suspenseBoundaryID);
16871687
// if (!suspenseIdNode) {
@@ -1693,6 +1693,7 @@ export function writeEndSegment(
16931693
// const suspenseNode = suspenseIdNode.previousSibling;
16941694
// // Tag it to be client rendered.
16951695
// suspenseNode.data = SUSPENSE_FALLBACK_START_DATA;
1696+
// suspenseNode.data2 = errorMsg;
16961697
// // Tell React to retry it if the parent already hydrated.
16971698
// if (suspenseNode._reactRetry) {
16981699
// suspenseNode._reactRetry();
@@ -1780,7 +1781,7 @@ const completeSegmentFunction =
17801781
const completeBoundaryFunction =
17811782
'function $RC(a,b){a=document.getElementById(a);b=document.getElementById(b);b.parentNode.removeChild(b);if(a){a=a.previousSibling;var f=a.parentNode,c=a.nextSibling,e=0;do{if(c&&8===c.nodeType){var d=c.data;if("/$"===d)if(0===e)break;else e--;else"$"!==d&&"$?"!==d&&"$!"!==d||e++}d=c.nextSibling;f.removeChild(c);c=d}while(c);for(;b.firstChild;)f.insertBefore(b.firstChild,c);a.data="$";a._reactRetry&&a._reactRetry()}}';
17821783
const clientRenderFunction =
1783-
'function $RX(a){if(a=document.getElementById(a))a=a.previousSibling,a.data="$!",a._reactRetry&&a._reactRetry()}';
1784+
'function $RX(a,b){if(a=document.getElementById(a))a=a.previousSibling,a.data="$!",a.data2=b,a._reactRetry&&a._reactRetry()}';
17841785

17851786
const completeSegmentScript1Full = stringToPrecomputedChunk(
17861787
completeSegmentFunction + ';$RS("',
@@ -1850,15 +1851,17 @@ export function writeCompletedBoundaryInstruction(
18501851
}
18511852

18521853
const clientRenderScript1Full = stringToPrecomputedChunk(
1853-
clientRenderFunction + ';$RX("',
1854+
clientRenderFunction + ";$RX('",
18541855
);
1855-
const clientRenderScript1Partial = stringToPrecomputedChunk('$RX("');
1856-
const clientRenderScript2 = stringToPrecomputedChunk('")</script>');
1856+
const clientRenderScript1Partial = stringToPrecomputedChunk("$RX('");
1857+
const clientRenderScript2 = stringToPrecomputedChunk("')</script>");
1858+
const clientRenderErrorScript1 = stringToPrecomputedChunk("','");
18571859

18581860
export function writeClientRenderBoundaryInstruction(
18591861
destination: Destination,
18601862
responseState: ResponseState,
18611863
boundaryID: SuspenseBoundaryID,
1864+
error: ?string,
18621865
): boolean {
18631866
writeChunk(destination, responseState.startInlineScript);
18641867
if (!responseState.sentClientRenderFunction) {
@@ -1877,5 +1880,9 @@ export function writeClientRenderBoundaryInstruction(
18771880
}
18781881

18791882
writeChunk(destination, boundaryID);
1883+
if (error) {
1884+
writeChunk(destination, clientRenderErrorScript1);
1885+
writeChunk(destination, error);
1886+
}
18801887
return writeChunkAndReturn(destination, clientRenderScript2);
18811888
}

packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,8 @@ export function writeClientRenderBoundaryInstruction(
284284
destination: Destination,
285285
responseState: ResponseState,
286286
boundaryID: SuspenseBoundaryID,
287+
// TODO: encode error for native
288+
error: ?string,
287289
): boolean {
288290
writeChunk(destination, SUSPENSE_UPDATE_TO_CLIENT_RENDER);
289291
return writeChunkAndReturn(destination, formatID(boundaryID));

packages/react-reconciler/src/ReactFiberBeginWork.new.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ import {
152152
shouldSetTextContent,
153153
isSuspenseInstancePending,
154154
isSuspenseInstanceFallback,
155+
getSuspenseInstanceFallbackError,
155156
registerSuspenseInstanceRetry,
156157
supportsHydration,
157158
isPrimaryRenderer,
@@ -2689,18 +2690,22 @@ function updateDehydratedSuspenseComponent(
26892690
// This boundary is in a permanent fallback state. In this case, we'll never
26902691
// get an update and we'll never be able to hydrate the final content. Let's just try the
26912692
// client side render instead.
2693+
const errorMsg = getSuspenseInstanceFallbackError(suspenseInstance);
26922694
return retrySuspenseComponentWithoutHydrating(
26932695
current,
26942696
workInProgress,
26952697
renderLanes,
26962698
// TODO: The server should serialize the error message so we can log it
26972699
// here on the client. Or, in production, a hash/id that corresponds to
26982700
// the error.
2699-
new Error(
2700-
'The server could not finish this Suspense boundary, likely ' +
2701-
'due to an error during server rendering. Switched to ' +
2702-
'client rendering.',
2703-
),
2701+
errorMsg
2702+
? // eslint-disable-next-line react-internal/prod-error-codes
2703+
new Error(errorMsg)
2704+
: new Error(
2705+
'The server could not finish this Suspense boundary, likely ' +
2706+
'due to an error during server rendering. Switched to ' +
2707+
'client rendering.',
2708+
),
27042709
);
27052710
}
27062711

packages/react-reconciler/src/ReactFiberBeginWork.old.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ import {
152152
shouldSetTextContent,
153153
isSuspenseInstancePending,
154154
isSuspenseInstanceFallback,
155+
getSuspenseInstanceFallbackError,
155156
registerSuspenseInstanceRetry,
156157
supportsHydration,
157158
isPrimaryRenderer,
@@ -2689,18 +2690,22 @@ function updateDehydratedSuspenseComponent(
26892690
// This boundary is in a permanent fallback state. In this case, we'll never
26902691
// get an update and we'll never be able to hydrate the final content. Let's just try the
26912692
// client side render instead.
2693+
const errorMsg = getSuspenseInstanceFallbackError(suspenseInstance);
26922694
return retrySuspenseComponentWithoutHydrating(
26932695
current,
26942696
workInProgress,
26952697
renderLanes,
26962698
// TODO: The server should serialize the error message so we can log it
26972699
// here on the client. Or, in production, a hash/id that corresponds to
26982700
// the error.
2699-
new Error(
2700-
'The server could not finish this Suspense boundary, likely ' +
2701-
'due to an error during server rendering. Switched to ' +
2702-
'client rendering.',
2703-
),
2701+
errorMsg
2702+
? // eslint-disable-next-line react-internal/prod-error-codes
2703+
new Error(errorMsg)
2704+
: new Error(
2705+
'The server could not finish this Suspense boundary, likely ' +
2706+
'due to an error during server rendering. Switched to ' +
2707+
'client rendering.',
2708+
),
27042709
);
27052710
}
27062711

0 commit comments

Comments
 (0)