Skip to content

Commit 49857bd

Browse files
acdlitezhengjitf
authored andcommitted
Log a recoverable error whenever hydration fails (facebook#23319)
There are several cases where hydration fails, server-rendered HTML is discarded, and we fall back to client rendering. Whenever this happens, we will now log an error with onRecoverableError, with a message explaining why. In some of these scenarios, this is not the only recoverable error that is logged. For example, an error during hydration will cause hydration to fail, which is itself an error. So we end up logging two separate errors: the original error, and one that explains why hydration failed. I've made sure that the original error always gets logged first, to preserve the causal sequence. Another thing we could do is aggregate the errors with the Error "cause" feature and AggregateError. Since these are new-ish features in JavaScript, we'd need a fallback behavior. I'll leave this for a follow up.
1 parent 265588b commit 49857bd

11 files changed

+315
-41
lines changed

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

+69-15
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,11 @@ describe('ReactDOMFizzServer', () => {
358358
window.__INIT__ = function() {
359359
bootstrapped = true;
360360
// Attempt to hydrate the content.
361-
ReactDOM.hydrateRoot(container, <App isClient={true} />);
361+
ReactDOM.hydrateRoot(container, <App isClient={true} />, {
362+
onRecoverableError(error) {
363+
Scheduler.unstable_yieldValue(error.message);
364+
},
365+
});
362366
};
363367

364368
await act(async () => {
@@ -394,7 +398,10 @@ describe('ReactDOMFizzServer', () => {
394398
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
395399

396400
// Now we can client render it instead.
397-
Scheduler.unstable_flushAll();
401+
expect(Scheduler).toFlushAndYield([
402+
'The server could not finish this Suspense boundary, likely due to ' +
403+
'an error during server rendering. Switched to client rendering.',
404+
]);
398405

399406
// The client rendered HTML is now in place.
400407
expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
@@ -465,7 +472,11 @@ describe('ReactDOMFizzServer', () => {
465472
expect(loggedErrors).toEqual([]);
466473

467474
// Attempt to hydrate the content.
468-
ReactDOM.hydrateRoot(container, <App isClient={true} />);
475+
ReactDOM.hydrateRoot(container, <App isClient={true} />, {
476+
onRecoverableError(error) {
477+
Scheduler.unstable_yieldValue(error.message);
478+
},
479+
});
469480
Scheduler.unstable_flushAll();
470481

471482
// We're still loading because we're waiting for the server to stream more content.
@@ -484,7 +495,10 @@ describe('ReactDOMFizzServer', () => {
484495
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
485496

486497
// Now we can client render it instead.
487-
Scheduler.unstable_flushAll();
498+
expect(Scheduler).toFlushAndYield([
499+
'The server could not finish this Suspense boundary, likely due to ' +
500+
'an error during server rendering. Switched to client rendering.',
501+
]);
488502

489503
// The client rendered HTML is now in place.
490504
expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
@@ -766,7 +780,11 @@ describe('ReactDOMFizzServer', () => {
766780
// We're still showing a fallback.
767781

768782
// Attempt to hydrate the content.
769-
ReactDOM.hydrateRoot(container, <App />);
783+
ReactDOM.hydrateRoot(container, <App />, {
784+
onRecoverableError(error) {
785+
Scheduler.unstable_yieldValue(error.message);
786+
},
787+
});
770788
Scheduler.unstable_flushAll();
771789

772790
// We're still loading because we're waiting for the server to stream more content.
@@ -778,7 +796,10 @@ describe('ReactDOMFizzServer', () => {
778796
});
779797

780798
// We still can't render it on the client.
781-
Scheduler.unstable_flushAll();
799+
expect(Scheduler).toFlushAndYield([
800+
'The server could not finish this Suspense boundary, likely due to an ' +
801+
'error during server rendering. Switched to client rendering.',
802+
]);
782803
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
783804

784805
// We now resolve it on the client.
@@ -1455,7 +1476,11 @@ describe('ReactDOMFizzServer', () => {
14551476
// We're still showing a fallback.
14561477

14571478
// Attempt to hydrate the content.
1458-
ReactDOM.hydrateRoot(container, <App isClient={true} />);
1479+
ReactDOM.hydrateRoot(container, <App isClient={true} />, {
1480+
onRecoverableError(error) {
1481+
Scheduler.unstable_yieldValue(error.message);
1482+
},
1483+
});
14591484
Scheduler.unstable_flushAll();
14601485

14611486
// We're still loading because we're waiting for the server to stream more content.
@@ -1484,7 +1509,10 @@ describe('ReactDOMFizzServer', () => {
14841509
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
14851510

14861511
// That will let us client render it instead.
1487-
Scheduler.unstable_flushAll();
1512+
expect(Scheduler).toFlushAndYield([
1513+
'The server could not finish this Suspense boundary, likely due to ' +
1514+
'an error during server rendering. Switched to client rendering.',
1515+
]);
14881516

14891517
// The client rendered HTML is now in place.
14901518
expect(getVisibleChildren(container)).toEqual(
@@ -1736,8 +1764,11 @@ describe('ReactDOMFizzServer', () => {
17361764
// The first paint switches to client rendering due to mismatch
17371765
expect(Scheduler).toFlushUntilNextPaint([
17381766
'client',
1739-
'Log recoverable error: An error occurred during hydration. ' +
1740-
'The server HTML was replaced with client content',
1767+
'Log recoverable error: Hydration failed because the initial ' +
1768+
'UI does not match what was rendered on the server.',
1769+
'Log recoverable error: There was an error while hydrating. ' +
1770+
'Because the error happened outside of a Suspense boundary, the ' +
1771+
'entire root will switch to client rendering.',
17411772
]);
17421773
}).toErrorDev(
17431774
[
@@ -1834,8 +1865,11 @@ describe('ReactDOMFizzServer', () => {
18341865
// The first paint switches to client rendering due to mismatch
18351866
expect(Scheduler).toFlushUntilNextPaint([
18361867
'client',
1837-
'Log recoverable error: An error occurred during hydration. ' +
1838-
'The server HTML was replaced with client content',
1868+
'Log recoverable error: Hydration failed because the initial ' +
1869+
'UI does not match what was rendered on the server.',
1870+
'Log recoverable error: There was an error while hydrating. ' +
1871+
'Because the error happened outside of a Suspense boundary, the ' +
1872+
'entire root will switch to client rendering.',
18391873
]);
18401874
}).toErrorDev(
18411875
[
@@ -1928,7 +1962,13 @@ describe('ReactDOMFizzServer', () => {
19281962
// An error logged but instead of surfacing it to the UI, we switched
19291963
// to client rendering.
19301964
expect(() => {
1931-
expect(Scheduler).toFlushAndYield(['Yay!', 'Hydration error']);
1965+
expect(Scheduler).toFlushAndYield([
1966+
'Yay!',
1967+
'Hydration error',
1968+
'There was an error while hydrating. Because the error happened ' +
1969+
'outside of a Suspense boundary, the entire root will switch ' +
1970+
'to client rendering.',
1971+
]);
19321972
}).toErrorDev(
19331973
'An error occurred during hydration. The server HTML was replaced',
19341974
{withoutStack: true},
@@ -2012,7 +2052,11 @@ describe('ReactDOMFizzServer', () => {
20122052

20132053
// An error logged but instead of surfacing it to the UI, we switched
20142054
// to client rendering.
2015-
expect(Scheduler).toFlushAndYield(['Yay!', 'Hydration error']);
2055+
expect(Scheduler).toFlushAndYield([
2056+
'Yay!',
2057+
'Hydration error',
2058+
'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
2059+
]);
20162060
expect(getVisibleChildren(container)).toEqual(
20172061
<div>
20182062
<span />
@@ -2178,7 +2222,11 @@ describe('ReactDOMFizzServer', () => {
21782222

21792223
// An error logged but instead of surfacing it to the UI, we switched
21802224
// to client rendering.
2181-
expect(Scheduler).toFlushAndYield(['Hydration error']);
2225+
expect(Scheduler).toFlushAndYield([
2226+
'Hydration error',
2227+
'There was an error while hydrating this Suspense boundary. Switched ' +
2228+
'to client rendering.',
2229+
]);
21822230
expect(getVisibleChildren(container)).toEqual(
21832231
<div>
21842232
<span />
@@ -2328,8 +2376,14 @@ describe('ReactDOMFizzServer', () => {
23282376
expect(Scheduler).toFlushAndYield([
23292377
'A',
23302378
'B',
2379+
23312380
'Logged recoverable error: Hydration error',
2381+
'Logged recoverable error: There was an error while hydrating this ' +
2382+
'Suspense boundary. Switched to client rendering.',
2383+
23322384
'Logged recoverable error: Hydration error',
2385+
'Logged recoverable error: There was an error while hydrating this ' +
2386+
'Suspense boundary. Switched to client rendering.',
23332387
]);
23342388
});
23352389
});

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

+10-2
Original file line numberDiff line numberDiff line change
@@ -232,15 +232,23 @@ describe('ReactDOMFizzShellHydration', () => {
232232

233233
// Hydration suspends because the data for the shell hasn't loaded yet
234234
const root = await clientAct(async () => {
235-
return ReactDOM.hydrateRoot(container, <App />);
235+
return ReactDOM.hydrateRoot(container, <App />, {
236+
onRecoverableError(error) {
237+
Scheduler.unstable_yieldValue(error.message);
238+
},
239+
});
236240
});
237241
expect(Scheduler).toHaveYielded(['Suspend! [Shell]']);
238242
expect(container.textContent).toBe('Shell');
239243

240244
await clientAct(async () => {
241245
root.render(<Text text="New screen" />);
242246
});
243-
expect(Scheduler).toHaveYielded(['New screen']);
247+
expect(Scheduler).toHaveYielded([
248+
'This root received an early update, before anything was able ' +
249+
'hydrate. Switched the entire root to client rendering.',
250+
'New screen',
251+
]);
244252
expect(container.textContent).toBe('New screen');
245253
});
246254
});

0 commit comments

Comments
 (0)