348
'Component',
349
350
// Hydration mismatch is logged
351
- 'An error occurred during hydration. The server HTML was replaced with client content',
351
+ 'Hydration failed because the initial UI does not match what was rendered on the server.',
352
+ 'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
353
]);
354
355
// Client rendered - suspense comment nodes removed
433
onDeleted(node) {
434
deleted.push(node);
435
},
436
+ onRecoverableError(error) {
437
+ Scheduler.unstable_yieldValue(error.message);
438
+ },
439
});
436
- Scheduler.unstable_flushAll();
440
+ expect(Scheduler).toFlushAndYield([]);
441
442
expect(hydrated.length).toBe(0);
443
expect(deleted.length).toBe(0);
457
458
Scheduler.unstable_flushAll();
459
jest.runAllTimers();
460
+ expect(Scheduler).toHaveYielded([
461
+ 'This Suspense boundary received an update before it finished ' +
462
+ 'hydrating. This caused the boundary to switch to client rendering. ' +
463
+ 'The usual way to fix this is to wrap the original update ' +
464
+ 'in startTransition.',
465
+ ]);
466
467
expect(hydrated.length).toBe(1);
468
expect(deleted.length).toBe(1);
517
518
expect(() => {
519
act(() => {
510
- ReactDOM.hydrateRoot(container, <App hasB={false} />);
520
+ ReactDOM.hydrateRoot(container, <App hasB={false} />, {
521
+ onRecoverableError(error) {
522
+ Scheduler.unstable_yieldValue(error.message);
523
+ },
524
+ });
525
});
526
}).toErrorDev('Did not expect server HTML to contain a <span> in <div>');
527
531
expect(container.innerHTML).not.toContain('<span>B</span>');
532
533
if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
534
+ expect(Scheduler).toHaveYielded([
535
+ 'There was an error while hydrating this Suspense boundary. ' +
536
+ 'Switched to client rendering.',
537
+ ]);
538
expect(ref.current).not.toBe(span);
539
} else {
540
expect(ref.current).toBe(span);
660
}).toErrorDev('Did not expect server HTML to contain a <span> in <div>');
661
if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
662
expect(Scheduler).toHaveYielded([
645
- 'An error occurred during hydration. The server HTML was replaced ' +
646
- 'with client content',
663
+ 'Hydration failed because the initial UI does not match what was rendered on the server.',
664
+ 'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
665
]);
666
}
667
1105
const root = ReactDOM.hydrateRoot(
1106
container,
1107
<App text="Hello" className="hello" />,
1108
+ {
1109
+ onRecoverableError(error) {
1110
+ Scheduler.unstable_yieldValue(error.message);
1111
+ },
1112
+ },
1113
);
1114
Scheduler.unstable_flushAll();
1115
jest.runAllTimers();
1120
root.render(<App text="Hi" className="hi" />);
1121
Scheduler.unstable_flushAll();
1122
jest.runAllTimers();
1123
+ expect(Scheduler).toHaveYielded([
1124
+ 'This Suspense boundary received an update before it finished ' +
1125
+ 'hydrating. This caused the boundary to switch to client ' +
1126
+ 'rendering. The usual way to fix this is to wrap the original ' +
1127
+ 'update in startTransition.',
1128
+ ]);
1129
1130
// Flushing now should delete the existing content and show the fallback.
1131
1191
const root = ReactDOM.hydrateRoot(
1192
container,
1193
<App text="Hello" className="hello" />,
1194
+ {
1195
+ onRecoverableError(error) {
1196
+ Scheduler.unstable_yieldValue(error.message);
1197
+ },
1198
+ },
1199
);
1200
Scheduler.unstable_flushAll();
1201
jest.runAllTimers();
1209
// Flushing now should delete the existing content and show the fallback.
1210
Scheduler.unstable_flushAll();
1211
jest.runAllTimers();
1212
+ expect(Scheduler).toHaveYielded([
1213
+ 'This Suspense boundary received an update before it finished ' +
1214
+ 'hydrating. This caused the boundary to switch to client rendering. ' +
1215
+ 'The usual way to fix this is to wrap the original update ' +
1216
+ 'in startTransition.',
1217
+ ]);
1218
1219
expect(container.getElementsByTagName('span').length).toBe(1);
1220
expect(ref.current).toBe(span);
1276
const root = ReactDOM.hydrateRoot(
1277
container,
1278
<App text="Hello" className="hello" />,
1279
+ {
1280
+ onRecoverableError(error) {
1281
+ Scheduler.unstable_yieldValue(error.message);
1282
+ },
1283
+ },
1284
);
1285
Scheduler.unstable_flushAll();
1286
jest.runAllTimers();
1302
suspend = false;
1303
resolve();
1304
await promise;
1305
+ expect(Scheduler).toHaveYielded([
1306
+ 'This Suspense boundary received an update before it finished ' +
1307
+ 'hydrating. This caused the boundary to switch to client rendering. ' +
1308
+ 'The usual way to fix this is to wrap the original update ' +
1309
+ 'in startTransition.',
1310
+ ]);
1311
1312
Scheduler.unstable_flushAll();
1313
jest.runAllTimers();
1596
<Context.Provider value={{text: 'Hello', className: 'hello'}}>
1597
<App />
1598
</Context.Provider>,
1599
+ {
1600
+ onRecoverableError(error) {
1601
+ Scheduler.unstable_yieldValue(error.message);
1602
+ },
1603
+ },
1604
);
1605
Scheduler.unstable_flushAll();
1606
jest.runAllTimers();
1617
// Flushing now should delete the existing content and show the fallback.
1618
Scheduler.unstable_flushAll();
1619
jest.runAllTimers();
1620
+ expect(Scheduler).toHaveYielded([
1621
+ 'This Suspense boundary received an update before it finished ' +
1622
+ 'hydrating. This caused the boundary to switch to client rendering. ' +
1623
+ 'The usual way to fix this is to wrap the original update ' +
1624
+ 'in startTransition.',
1625
+ ]);
1626
1627
expect(container.getElementsByTagName('span').length).toBe(0);
1628
expect(ref.current).toBe(null);
1680
1681
// On the client we have the data available quickly for some reason.
1682
suspend = false;
1621
- ReactDOM.hydrateRoot(container, <App />);
1622
- Scheduler.unstable_flushAll();
1683
+ ReactDOM.hydrateRoot(container, <App />, {
1684
+ onRecoverableError(error) {
1685
+ Scheduler.unstable_yieldValue(error.message);
1686
+ },
1687
+ });
1688
+ expect(Scheduler).toFlushAndYield([
1689
+ 'The server could not finish this Suspense boundary, likely due to ' +
1690
+ 'an error during server rendering. Switched to client rendering.',
1691
+ ]);
1692
jest.runAllTimers();
1693
1694
expect(container.textContent).toBe('Hello');
1742
1743
// On the client we have the data available quickly for some reason.
1744
suspend = false;
1676
- ReactDOM.hydrateRoot(container, <App />);
1677
- Scheduler.unstable_flushAll();
1745
+ ReactDOM.hydrateRoot(container, <App />, {
1746
+ onRecoverableError(error) {
1747
+ Scheduler.unstable_yieldValue(error.message);
1748
+ },
1749
+ });
1750
+ expect(Scheduler).toFlushAndYield([
1751
+ 'The server could not finish this Suspense boundary, likely due to ' +
1752
+ 'an error during server rendering. Switched to client rendering.',
1753
+ ]);
1754
// This will have exceeded the suspended time so we should timeout.
1755
jest.advanceTimersByTime(500);
1756
// The boundary should longer be suspended for the middle content
1809
1810
// On the client we have the data available quickly for some reason.
1811
suspend = false;
1736
- ReactDOM.hydrateRoot(container, <App />);
1737
- Scheduler.unstable_flushAll();
1812
+ ReactDOM.hydrateRoot(container, <App />, {
1813
+ onRecoverableError(error) {
1814
+ Scheduler.unstable_yieldValue(error.message);
1815
+ },
1816
+ });
1817
+ expect(Scheduler).toFlushAndYield([
1818
+ 'The server could not finish this Suspense boundary, likely due to ' +
1819
+ 'an error during server rendering. Switched to client rendering.',
1820
+ ]);
1821
// This will have exceeded the suspended time so we should timeout.
1822
jest.advanceTimersByTime(500);
1823
// The boundary should longer be suspended for the middle content
2119
const container = document.createElement('div');
2120
container.innerHTML = html;
2121
2039
- ReactDOM.hydrateRoot(container, <App />);
2122
+ ReactDOM.hydrateRoot(container, <App />, {
2123
+ onRecoverableError(error) {
2124
+ Scheduler.unstable_yieldValue(error.message);
2125
+ },
2126
+ });
2127
2128
suspend = true;
2042
- Scheduler.unstable_flushAll();
2129
+ expect(Scheduler).toFlushAndYield([
2130
+ 'The server could not finish this Suspense boundary, likely due to ' +
2131
+ 'an error during server rendering. Switched to client rendering.',
2132
+ ]);
2133
2134
// We haven't hydrated the second child but the placeholder is still in the list.
2135
expect(container.textContent).toBe('ALoading B');
2184
const span = container.getElementsByTagName('span')[1];
2185
2186
suspend = false;
2097
- ReactDOM.hydrateRoot(container, <App />);
2098
- Scheduler.unstable_flushAll();
2187
+ ReactDOM.hydrateRoot(container, <App />, {
2188
+ onRecoverableError(error) {
2189
+ Scheduler.unstable_yieldValue(error.message);
2190
+ },
2191
+ });
2192
+ expect(Scheduler).toFlushAndYield([
2193
+ 'The server could not finish this Suspense boundary, likely due to ' +
2194
+ 'an error during server rendering. Switched to client rendering.',
2195
+ ]);
2196
jest.runAllTimers();
2197
2198
expect(ref.current).toBe(span);
2290
<ClassName.Provider value={'hello'}>
2291
<App text="Hello" />
2292
</ClassName.Provider>,
2293
+ {
2294
+ onRecoverableError(error) {
2295
+ Scheduler.unstable_yieldValue(error.message);
2296
+ },
2297
+ },
2298
);
2299
Scheduler.unstable_flushAll();
2300
jest.runAllTimers();
2314
// This will force all expiration times to flush.
2315
Scheduler.unstable_flushAll();
2316
jest.runAllTimers();
2317
+ expect(Scheduler).toHaveYielded([
2318
+ 'This Suspense boundary received an update before it finished ' +
2319
+ 'hydrating. This caused the boundary to switch to client rendering. ' +
2320
+ 'The usual way to fix this is to wrap the original update ' +
2321
+ 'in startTransition.',
2322
+ ]);
2323
2324
// This will now be a new span because we weren't able to hydrate before
2325
const newSpan = container.getElementsByTagName('span')[0];
3340
{withoutStack: 1},
3341
);
3342
expect(Scheduler).toHaveYielded([
3235
- 'Log recoverable error: An error occurred during hydration. The server ' +
3236
- 'HTML was replaced with client content',
3343
+ 'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.',
3344
// TODO: There were multiple mismatches in a single container. Should
3345
// we attempt to de-dupe them?
3239
- 'Log recoverable error: An error occurred during hydration. The server ' +
3240
- 'HTML was replaced with client content',
3346
+ 'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.',
3347
+ 'Log recoverable error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
3348
]);
3349
3350
// We show fallback state when mismatch happens at root