Skip to content

Commit c17a9ce

Browse files
committed
extend onRecoverableError API to support errorInfo
errorInfo has been used in Error Boundaries wiht componentDidCatch for a while now. To date this metadata only contained a componentStack. onRecoverableError only receives an error (type mixed) argument and thus providing additional error metadata was not possible without mutating user created mixed objects. This change modifies rootConcurrentErrors rootRecoverableErrors, and hydrationErrors so all expect CapturedValue types. additionally a new factory function allows the creation of CapturedValues from a value plus a hash and stack. In general, client derived CapturedValues will be created using the original function which derives a componentStack from a fiber and server originated CapturedValues will be created using with a passed in hash and optional componentStack.
1 parent aec5759 commit c17a9ce

12 files changed

+280
-168
lines changed

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

+119-72
Original file line numberDiff line numberDiff line change
@@ -90,46 +90,28 @@ describe('ReactDOMFizzServer', () => {
9090
});
9191

9292
function expectErrors(errorsArr, toBeDevArr, toBeProdArr) {
93-
const mappedErrows = errorsArr.map(error => {
94-
if (error.componentStack) {
95-
return [
96-
error.message,
97-
error.hash,
98-
normalizeCodeLocInfo(error.componentStack),
99-
];
100-
} else if (error.hash) {
101-
return [error.message, error.hash];
93+
const mappedErrows = errorsArr.map(({error, errorInfo}) => {
94+
const stack = errorInfo && errorInfo.componentStack;
95+
const errorHash = errorInfo && errorInfo.errorHash;
96+
if (stack) {
97+
return [error.message, errorHash, normalizeCodeLocInfo(stack)];
98+
} else if (errorHash) {
99+
return [error.message, errorHash];
102100
}
103101
return error.message;
104102
});
105103
if (__DEV__) {
106-
expect(mappedErrows).toEqual(
107-
toBeDevArr,
108-
// .map(([errorMessage, errorHash, errorComponentStack]) => {
109-
// if (typeof error === 'string' || error instanceof String) {
110-
// return error;
111-
// }
112-
// let str = JSON.stringify(error).replace(/\\n/g, '\n');
113-
// // this gets stripped away by normalizeCodeLocInfo...
114-
// // Kind of hacky but lets strip it away here too just so they match...
115-
// // easier than fixing the regex to account for this edge case
116-
// if (str.endsWith('at **)"}')) {
117-
// str = str.replace(/at \*\*\)\"}$/, 'at **)');
118-
// }
119-
// return str;
120-
// }),
121-
);
104+
expect(mappedErrows).toEqual(toBeDevArr);
122105
} else {
123106
expect(mappedErrows).toEqual(toBeProdArr);
124107
}
125108
}
126109

127-
// @TODO we will use this in a followup change once we start exposing componentStacks from server errors
128-
// function componentStack(components) {
129-
// return components
130-
// .map(component => `\n in ${component} (at **)`)
131-
// .join('');
132-
// }
110+
function componentStack(components) {
111+
return components
112+
.map(component => `\n in ${component} (at **)`)
113+
.join('');
114+
}
133115

134116
async function act(callback) {
135117
await callback();
@@ -471,8 +453,8 @@ describe('ReactDOMFizzServer', () => {
471453
bootstrapped = true;
472454
// Attempt to hydrate the content.
473455
ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
474-
onRecoverableError(error) {
475-
errors.push(error);
456+
onRecoverableError(error, errorInfo) {
457+
errors.push({error, errorInfo});
476458
},
477459
});
478460
};
@@ -483,8 +465,8 @@ describe('ReactDOMFizzServer', () => {
483465
loggedErrors.push(x);
484466
return 'Hash of (' + x.message + ')';
485467
}
486-
// const expectedHash = onError(theError);
487-
// loggedErrors.length = 0;
468+
const expectedHash = onError(theError);
469+
loggedErrors.length = 0;
488470

489471
await act(async () => {
490472
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
@@ -519,9 +501,18 @@ describe('ReactDOMFizzServer', () => {
519501
expect(Scheduler).toFlushAndYield([]);
520502
expectErrors(
521503
errors,
522-
[theError.message],
523504
[
524-
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
505+
[
506+
theError.message,
507+
expectedHash,
508+
componentStack(['Lazy', 'Suspense', 'div', 'App']),
509+
],
510+
],
511+
[
512+
[
513+
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
514+
expectedHash,
515+
],
525516
],
526517
);
527518

@@ -577,8 +568,8 @@ describe('ReactDOMFizzServer', () => {
577568
loggedErrors.push(x);
578569
return 'hash of (' + x.message + ')';
579570
}
580-
// const expectedHash = onError(theError);
581-
// loggedErrors.length = 0;
571+
const expectedHash = onError(theError);
572+
loggedErrors.length = 0;
582573

583574
function App({isClient}) {
584575
return (
@@ -605,8 +596,8 @@ describe('ReactDOMFizzServer', () => {
605596
const errors = [];
606597
// Attempt to hydrate the content.
607598
ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
608-
onRecoverableError(error) {
609-
errors.push(error);
599+
onRecoverableError(error, errorInfo) {
600+
errors.push({error, errorInfo});
610601
},
611602
});
612603
Scheduler.unstable_flushAll();
@@ -630,9 +621,18 @@ describe('ReactDOMFizzServer', () => {
630621

631622
expectErrors(
632623
errors,
633-
[theError.message],
634624
[
635-
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
625+
[
626+
theError.message,
627+
expectedHash,
628+
componentStack(['Suspense', 'div', 'App']),
629+
],
630+
],
631+
[
632+
[
633+
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
634+
expectedHash,
635+
],
636636
],
637637
);
638638

@@ -675,8 +675,8 @@ describe('ReactDOMFizzServer', () => {
675675
loggedErrors.push(x);
676676
return 'hash(' + x.message + ')';
677677
}
678-
// const expectedHash = onError(theError);
679-
// loggedErrors.length = 0;
678+
const expectedHash = onError(theError);
679+
loggedErrors.length = 0;
680680

681681
await act(async () => {
682682
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
@@ -693,8 +693,8 @@ describe('ReactDOMFizzServer', () => {
693693
const errors = [];
694694
// Attempt to hydrate the content.
695695
ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
696-
onRecoverableError(error) {
697-
errors.push(error);
696+
onRecoverableError(error, errorInfo) {
697+
errors.push({error, errorInfo});
698698
},
699699
});
700700
Scheduler.unstable_flushAll();
@@ -703,9 +703,18 @@ describe('ReactDOMFizzServer', () => {
703703

704704
expectErrors(
705705
errors,
706-
[theError.message],
707706
[
708-
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
707+
[
708+
theError.message,
709+
expectedHash,
710+
componentStack(['Erroring', 'Suspense', 'div', 'App']),
711+
],
712+
],
713+
[
714+
[
715+
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
716+
expectedHash,
717+
],
709718
],
710719
);
711720
});
@@ -735,8 +744,8 @@ describe('ReactDOMFizzServer', () => {
735744
loggedErrors.push(x);
736745
return 'hash(' + x.message + ')';
737746
}
738-
// const expectedHash = onError(theError);
739-
// loggedErrors.length = 0;
747+
const expectedHash = onError(theError);
748+
loggedErrors.length = 0;
740749

741750
await act(async () => {
742751
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
@@ -753,8 +762,8 @@ describe('ReactDOMFizzServer', () => {
753762
const errors = [];
754763
// Attempt to hydrate the content.
755764
ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
756-
onRecoverableError(error) {
757-
errors.push(error);
765+
onRecoverableError(error, errorInfo) {
766+
errors.push({error, errorInfo});
758767
},
759768
});
760769
Scheduler.unstable_flushAll();
@@ -773,9 +782,18 @@ describe('ReactDOMFizzServer', () => {
773782

774783
expectErrors(
775784
errors,
776-
[theError.message],
777785
[
778-
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
786+
[
787+
theError.message,
788+
expectedHash,
789+
componentStack(['Lazy', 'Suspense', 'div', 'App']),
790+
],
791+
],
792+
[
793+
[
794+
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
795+
expectedHash,
796+
],
779797
],
780798
);
781799

@@ -1053,9 +1071,10 @@ describe('ReactDOMFizzServer', () => {
10531071
}
10541072

10551073
const loggedErrors = [];
1074+
const expectedHash = 'Hash for Abort';
10561075
function onError(error) {
10571076
loggedErrors.push(error);
1058-
return `Hash of (${error.message})`;
1077+
return expectedHash;
10591078
}
10601079

10611080
let controls;
@@ -1069,8 +1088,8 @@ describe('ReactDOMFizzServer', () => {
10691088
const errors = [];
10701089
// Attempt to hydrate the content.
10711090
ReactDOMClient.hydrateRoot(container, <App />, {
1072-
onRecoverableError(error) {
1073-
errors.push(error);
1091+
onRecoverableError(error, errorInfo) {
1092+
errors.push({error, errorInfo});
10741093
},
10751094
});
10761095
Scheduler.unstable_flushAll();
@@ -1087,9 +1106,12 @@ describe('ReactDOMFizzServer', () => {
10871106
expect(Scheduler).toFlushAndYield([]);
10881107
expectErrors(
10891108
errors,
1090-
['This Suspense boundary was aborted by the server'],
1109+
[['This Suspense boundary was aborted by the server', expectedHash]],
10911110
[
1092-
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
1111+
[
1112+
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
1113+
expectedHash,
1114+
],
10931115
],
10941116
);
10951117
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
@@ -1755,8 +1777,8 @@ describe('ReactDOMFizzServer', () => {
17551777
loggedErrors.push(x);
17561778
return `hash of (${x.message})`;
17571779
}
1758-
// const expectedHash = onError(theError);
1759-
// loggedErrors.length = 0;
1780+
const expectedHash = onError(theError);
1781+
loggedErrors.length = 0;
17601782

17611783
let controls;
17621784
await act(async () => {
@@ -1775,8 +1797,8 @@ describe('ReactDOMFizzServer', () => {
17751797
const errors = [];
17761798
// Attempt to hydrate the content.
17771799
ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
1778-
onRecoverableError(error) {
1779-
errors.push(error);
1800+
onRecoverableError(error, errorInfo) {
1801+
errors.push({error, errorInfo});
17801802
},
17811803
});
17821804
Scheduler.unstable_flushAll();
@@ -1809,9 +1831,25 @@ describe('ReactDOMFizzServer', () => {
18091831
expect(Scheduler).toFlushAndYield([]);
18101832
expectErrors(
18111833
errors,
1812-
[theError.message],
18131834
[
1814-
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
1835+
[
1836+
theError.message,
1837+
expectedHash,
1838+
componentStack([
1839+
'AsyncText',
1840+
'h1',
1841+
'Suspense',
1842+
'div',
1843+
'Suspense',
1844+
'App',
1845+
]),
1846+
],
1847+
],
1848+
[
1849+
[
1850+
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
1851+
expectedHash,
1852+
],
18151853
],
18161854
);
18171855

@@ -3142,8 +3180,8 @@ describe('ReactDOMFizzServer', () => {
31423180
loggedErrors.push(x);
31433181
return x.message.replace('bad message', 'bad hash');
31443182
}
3145-
// const expectedHash = onError(theError);
3146-
// loggedErrors.length = 0;
3183+
const expectedHash = onError(theError);
3184+
loggedErrors.length = 0;
31473185

31483186
await act(async () => {
31493187
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />, {
@@ -3156,18 +3194,27 @@ describe('ReactDOMFizzServer', () => {
31563194

31573195
const errors = [];
31583196
ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
3159-
onRecoverableError(error) {
3160-
errors.push(error);
3197+
onRecoverableError(error, errorInfo) {
3198+
errors.push({error, errorInfo});
31613199
},
31623200
});
31633201
expect(Scheduler).toFlushAndYield([]);
31643202

31653203
// If escaping were not done we would get a message that says "bad hash"
31663204
expectErrors(
31673205
errors,
3168-
[theError.message],
31693206
[
3170-
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
3207+
[
3208+
theError.message,
3209+
expectedHash,
3210+
componentStack(['Erroring', 'Suspense', 'div', 'App']),
3211+
],
3212+
],
3213+
[
3214+
[
3215+
'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
3216+
expectedHash,
3217+
],
31713218
],
31723219
);
31733220
});

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

+7-14
Original file line numberDiff line numberDiff line change
@@ -731,27 +731,20 @@ export function isSuspenseInstanceFallback(instance: SuspenseInstance) {
731731
}
732732
export function getSuspenseInstanceFallbackErrorDetails(
733733
instance: SuspenseInstance,
734-
) {
734+
): {message?: string, stack?: string, hash?: string} {
735735
const nextSibling = instance.nextSibling;
736-
let errorMessage /*, errorComponentStack, errorHash*/;
737736
if (
738737
nextSibling &&
739738
nextSibling.nodeType === ELEMENT_NODE &&
740739
nextSibling.nodeName.toLowerCase() === 'template'
741740
) {
742-
const msg = ((nextSibling: any): HTMLTemplateElement).dataset.msg;
743-
if (msg !== null) errorMessage = msg;
744-
745-
// @TODO read and return hash and componentStack once we know how we are goign to
746-
// expose this extra errorInfo to onRecoverableError
747-
748-
// const hash = ((nextSibling: any): HTMLTemplateElement).dataset.hash;
749-
// if (hash !== null) errorHash = hash;
750-
751-
// const stack = ((nextSibling: any): HTMLTemplateElement).dataset.stack;
752-
// if (stack !== null) errorComponentStack = stack;
741+
return {
742+
message: ((nextSibling: any): HTMLTemplateElement).dataset.msg,
743+
stack: ((nextSibling: any): HTMLTemplateElement).dataset.stack,
744+
hash: ((nextSibling: any): HTMLTemplateElement).dataset.hash,
745+
};
753746
}
754-
return {errorMessage /*, errorComponentStack, errorHash*/};
747+
return {};
755748
}
756749

757750
export function registerSuspenseInstanceRetry(

0 commit comments

Comments
 (0)