Add back warning with component stack on Hydration mismatch (#23241)
* add back warning * wrapper errorMock in __DEV__ flag * lint
salazarm committed
Feb 7, 2022 at 17:25 UTC
1dece523505390a2e0da5f93db6d0c37287e02c4
4 files changed
+202
-125
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+14
-4
@@ -1740,8 +1740,13 @@ describe('ReactDOMFizzServer', () => {
1740
'The server HTML was replaced with client content',
1741
]);
1742
}).toErrorDev(
1743
- 'Warning: An error occurred during hydration. The server HTML was replaced with client content',
1744
- {withoutStack: true},
1743
+ [
1744
+ 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.',
1745
+ 'Warning: Expected server HTML to contain a matching <div> in <div>.\n' +
1746
+ ' in div (at **)\n' +
1747
+ ' in App (at **)',
1748
+ ],
1749
+ {withoutStack: 1},
1750
);
1751
expect(getVisibleChildren(container)).toEqual(<div>client</div>);
1752
} else {
@@ -1833,8 +1838,13 @@ describe('ReactDOMFizzServer', () => {
1838
'The server HTML was replaced with client content',
1839
]);
1840
}).toErrorDev(
1836
- 'Warning: An error occurred during hydration. The server HTML was replaced with client content',
1837
- {withoutStack: true},
1841
+ [
1842
+ 'Warning: An error occurred during hydration. The server HTML was replaced with client content',
1843
+ 'Warning: Expected server HTML to contain a matching <div> in <div>.\n' +
1844
+ ' in div (at **)\n' +
1845
+ ' in App (at **)',
1846
+ ],
1847
+ {withoutStack: 1},
1848
);
1849
expect(getVisibleChildren(container)).toEqual(<div>client</div>);
1850
} else {
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+152
-99
@@ -19,6 +19,15 @@ let SuspenseList;
19
let act;
20
let IdleEventPriority;
21
22
+function normalizeCodeLocInfo(strOrErr) {
23
+ if (strOrErr && strOrErr.replace) {
24
+ return strOrErr.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
25
+ return '\n in ' + name + ' (at **)';
26
+ });
27
+ }
28
+ return strOrErr;
29
+}
30
+
31
function dispatchMouseEvent(to, from) {
32
if (!to) {
33
to = null;
@@ -240,6 +249,12 @@ describe('ReactDOMServerPartialHydration', () => {
249
250
// @gate enableClientRenderFallbackOnHydrationMismatch
251
it('falls back to client rendering boundary on mismatch', async () => {
252
+ // We can't use the toErrorDev helper here because this is async.
253
+ const originalConsoleError = console.error;
254
+ const mockError = jest.fn();
255
+ console.error = (...args) => {
256
+ mockError(...args.map(normalizeCodeLocInfo));
257
+ };
258
let client = false;
259
let suspend = false;
260
let resolve;
@@ -276,70 +291,86 @@ describe('ReactDOMServerPartialHydration', () => {
291
</Suspense>
292
);
293
}
279
- const finalHTML = ReactDOMServer.renderToString(<App />);
280
- const container = document.createElement('div');
281
- container.innerHTML = finalHTML;
282
- expect(Scheduler).toHaveYielded([
283
- 'Hello',
284
- 'Component',
285
- 'Component',
286
- 'Component',
287
- 'Component',
288
- ]);
294
+ try {
295
+ const finalHTML = ReactDOMServer.renderToString(<App />);
296
+ const container = document.createElement('div');
297
+ container.innerHTML = finalHTML;
298
+ expect(Scheduler).toHaveYielded([
299
+ 'Hello',
300
+ 'Component',
301
+ 'Component',
302
+ 'Component',
303
+ 'Component',
304
+ ]);
305
290
- expect(container.innerHTML).toBe(
291
- '<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
292
- );
306
+ expect(container.innerHTML).toBe(
307
+ '<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
308
+ );
309
294
- suspend = true;
295
- client = true;
310
+ suspend = true;
311
+ client = true;
312
297
- ReactDOM.hydrateRoot(container, <App />, {
298
- onRecoverableError(error) {
299
- Scheduler.unstable_yieldValue(error.message);
300
- },
301
- });
302
- expect(Scheduler).toFlushAndYield([
303
- 'Suspend',
304
- 'Component',
305
- 'Component',
306
- 'Component',
307
- 'Component',
308
- ]);
309
- jest.runAllTimers();
313
+ ReactDOM.hydrateRoot(container, <App />, {
314
+ onRecoverableError(error) {
315
+ Scheduler.unstable_yieldValue(error.message);
316
+ },
317
+ });
318
+ expect(Scheduler).toFlushAndYield([
319
+ 'Suspend',
320
+ 'Component',
321
+ 'Component',
322
+ 'Component',
323
+ 'Component',
324
+ ]);
325
+ jest.runAllTimers();
326
311
- // Unchanged
312
- expect(container.innerHTML).toBe(
313
- '<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
314
- );
327
+ // Unchanged
328
+ expect(container.innerHTML).toBe(
329
+ '<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
330
+ );
331
316
- suspend = false;
317
- resolve();
318
- await promise;
332
+ suspend = false;
333
+ resolve();
334
+ await promise;
335
+ expect(Scheduler).toFlushAndYield([
336
+ // first pass, mismatches at end
337
+ 'Hello',
338
+ 'Component',
339
+ 'Component',
340
+ 'Component',
341
+ 'Component',
342
+
343
+ // second pass as client render
344
+ 'Hello',
345
+ 'Component',
346
+ 'Component',
347
+ 'Component',
348
+ 'Component',
349
+
350
+ // Hydration mismatch is logged
351
+ 'An error occurred during hydration. The server HTML was replaced with client content',
352
+ ]);
353
320
- expect(Scheduler).toFlushAndYield([
321
- // first pass, mismatches at end
322
- 'Hello',
323
- 'Component',
324
- 'Component',
325
- 'Component',
326
- 'Component',
327
-
328
- // second pass as client render
329
- 'Hello',
330
- 'Component',
331
- 'Component',
332
- 'Component',
333
- 'Component',
334
-
335
- // Hydration mismatch is logged
336
- 'An error occurred during hydration. The server HTML was replaced with client content',
337
- ]);
354
+ // Client rendered - suspense comment nodes removed
355
+ expect(container.innerHTML).toBe(
356
+ 'Hello<div>Component</div><div>Component</div><div>Component</div><article>Mismatch</article>',
357
+ );
358
339
- // Client rendered - suspense comment nodes removed
340
- expect(container.innerHTML).toBe(
341
- 'Hello<div>Component</div><div>Component</div><div>Component</div><article>Mismatch</article>',
342
- );
359
+ if (__DEV__) {
360
+ expect(mockError.mock.calls[0]).toEqual([
361
+ 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
362
+ 'div',
363
+ 'div',
364
+ '\n' +
365
+ ' in div (at **)\n' +
366
+ ' in Component (at **)\n' +
367
+ ' in Suspense (at **)\n' +
368
+ ' in App (at **)',
369
+ ]);
370
+ }
371
+ } finally {
372
+ console.error = originalConsoleError;
373
+ }
374
});
375
376
it('calls the hydration callbacks after hydration or deletion', async () => {
@@ -493,21 +524,14 @@ describe('ReactDOMServerPartialHydration', () => {
524
});
525
526
it('recovers with client render when server rendered additional nodes at suspense root after unsuspending', async () => {
496
- spyOnDev(console, 'error');
497
- const ref = React.createRef();
498
- function App({hasB}) {
499
- return (
500
- <div>
501
- <Suspense fallback="Loading...">
502
- <Suspender />
503
- <span ref={ref}>A</span>
504
- {hasB ? <span>B</span> : null}
505
- </Suspense>
506
- <div>Sibling</div>
507
- </div>
508
- );
509
- }
527
+ // We can't use the toErrorDev helper here because this is async.
528
+ const originalConsoleError = console.error;
529
+ const mockError = jest.fn();
530
+ console.error = (...args) => {
531
+ mockError(...args.map(normalizeCodeLocInfo));
532
+ };
533
534
+ const ref = React.createRef();
535
let shouldSuspend = false;
536
let resolve;
537
const promise = new Promise(res => {
@@ -522,37 +546,61 @@ describe('ReactDOMServerPartialHydration', () => {
546
}
547
return <></>;
548
}
549
+ function App({hasB}) {
550
+ return (
551
+ <div>
552
+ <Suspense fallback="Loading...">
553
+ <Suspender />
554
+ <span ref={ref}>A</span>
555
+ {hasB ? <span>B</span> : null}
556
+ </Suspense>
557
+ <div>Sibling</div>
558
+ </div>
559
+ );
560
+ }
561
+ try {
562
+ const finalHTML = ReactDOMServer.renderToString(<App hasB={true} />);
563
526
- const finalHTML = ReactDOMServer.renderToString(<App hasB={true} />);
527
-
528
- const container = document.createElement('div');
529
- container.innerHTML = finalHTML;
564
+ const container = document.createElement('div');
565
+ container.innerHTML = finalHTML;
566
531
- const span = container.getElementsByTagName('span')[0];
567
+ const span = container.getElementsByTagName('span')[0];
568
533
- expect(container.innerHTML).toContain('<span>A</span>');
534
- expect(container.innerHTML).toContain('<span>B</span>');
535
- expect(ref.current).toBe(null);
569
+ expect(container.innerHTML).toContain('<span>A</span>');
570
+ expect(container.innerHTML).toContain('<span>B</span>');
571
+ expect(ref.current).toBe(null);
572
537
- shouldSuspend = true;
538
- act(() => {
539
- ReactDOM.hydrateRoot(container, <App hasB={false} />);
540
- });
573
+ shouldSuspend = true;
574
+ act(() => {
575
+ ReactDOM.hydrateRoot(container, <App hasB={false} />);
576
+ });
577
542
- // await expect(async () => {
543
- resolve();
544
- await promise;
545
- Scheduler.unstable_flushAll();
546
- await null;
547
- jest.runAllTimers();
548
- // }).toErrorDev('Did not expect server HTML to contain a <span> in <div>');
578
+ resolve();
579
+ await promise;
580
+ Scheduler.unstable_flushAll();
581
+ await null;
582
+ jest.runAllTimers();
583
550
- expect(container.innerHTML).toContain('<span>A</span>');
551
- expect(container.innerHTML).not.toContain('<span>B</span>');
552
- if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
553
- expect(ref.current).not.toBe(span);
554
- } else {
555
- expect(ref.current).toBe(span);
584
+ expect(container.innerHTML).toContain('<span>A</span>');
585
+ expect(container.innerHTML).not.toContain('<span>B</span>');
586
+ if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
587
+ expect(ref.current).not.toBe(span);
588
+ } else {
589
+ expect(ref.current).toBe(span);
590
+ }
591
+ if (__DEV__) {
592
+ expect(mockError).toHaveBeenCalledWith(
593
+ 'Warning: Did not expect server HTML to contain a <%s> in <%s>.%s',
594
+ 'span',
595
+ 'div',
596
+ '\n' +
597
+ ' in Suspense (at **)\n' +
598
+ ' in div (at **)\n' +
599
+ ' in App (at **)',
600
+ );
601
+ }
602
+ } finally {
603
+ console.error = originalConsoleError;
604
}
605
});
606
@@ -3179,9 +3227,14 @@ describe('ReactDOMServerPartialHydration', () => {
3227
});
3228
});
3229
}).toErrorDev(
3182
- 'Warning: An error occurred during hydration. ' +
3183
- 'The server HTML was replaced with client content in <div>.',
3184
- {withoutStack: true},
3230
+ [
3231
+ 'Warning: An error occurred during hydration. ' +
3232
+ 'The server HTML was replaced with client content in <div>.',
3233
+ 'Warning: Expected server HTML to contain a matching <span> in <div>.\n' +
3234
+ ' in span (at **)\n' +
3235
+ ' in App (at **)',
3236
+ ],
3237
+ {withoutStack: 1},
3238
);
3239
expect(Scheduler).toHaveYielded([
3240
'Log recoverable error: An error occurred during hydration. The server ' +
packages/react-reconciler/src/ReactFiberHydrationContext.new.js
+18
-11
@@ -183,8 +183,7 @@ function deleteHydratableInstance(
183
}
184
}
185
186
-function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
187
- fiber.flags = (fiber.flags & ~Hydrating) | Placement;
186
+function warnNonhydratedInstance(returnFiber: Fiber, fiber: Fiber) {
187
if (__DEV__) {
188
switch (returnFiber.tag) {
189
case HostRoot: {
@@ -283,6 +282,10 @@ function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
282
}
283
}
284
}
285
+function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
286
+ fiber.flags = (fiber.flags & ~Hydrating) | Placement;
287
+ warnNonhydratedInstance(returnFiber, fiber);
288
+}
289
290
function tryHydrate(fiber, nextInstance) {
291
switch (fiber.tag) {
@@ -353,12 +356,10 @@ function shouldClientRenderOnMismatch(fiber: Fiber) {
356
);
357
}
358
356
-function throwOnHydrationMismatchIfConcurrentMode(fiber: Fiber) {
357
- if (shouldClientRenderOnMismatch(fiber)) {
358
- throw new Error(
359
- 'An error occurred during hydration. The server HTML was replaced with client content',
360
- );
361
- }
359
+function throwOnHydrationMismatch(fiber: Fiber) {
360
+ throw new Error(
361
+ 'An error occurred during hydration. The server HTML was replaced with client content',
362
+ );
363
}
364
365
function tryToClaimNextHydratableInstance(fiber: Fiber): void {
@@ -367,7 +368,10 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
368
}
369
let nextInstance = nextHydratableInstance;
370
if (!nextInstance) {
370
- throwOnHydrationMismatchIfConcurrentMode(fiber);
371
+ if (shouldClientRenderOnMismatch(fiber)) {
372
+ warnNonhydratedInstance((hydrationParentFiber: any), fiber);
373
+ throwOnHydrationMismatch(fiber);
374
+ }
375
// Nothing to hydrate. Make it an insertion.
376
insertNonHydratedInstance((hydrationParentFiber: any), fiber);
377
isHydrating = false;
@@ -376,7 +380,10 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
380
}
381
const firstAttemptedInstance = nextInstance;
382
if (!tryHydrate(fiber, nextInstance)) {
379
- throwOnHydrationMismatchIfConcurrentMode(fiber);
383
+ if (shouldClientRenderOnMismatch(fiber)) {
384
+ warnNonhydratedInstance((hydrationParentFiber: any), fiber);
385
+ throwOnHydrationMismatch(fiber);
386
+ }
387
// If we can't hydrate this instance let's try the next one.
388
// We use this as a heuristic. It's based on intuition and not data so it
389
// might be flawed or unnecessary.
@@ -565,7 +572,7 @@ function popHydrationState(fiber: Fiber): boolean {
572
if (nextInstance) {
573
if (shouldClientRenderOnMismatch(fiber)) {
574
warnIfUnhydratedTailNodes(fiber);
568
- throwOnHydrationMismatchIfConcurrentMode(fiber);
575
+ throwOnHydrationMismatch(fiber);
576
} else {
577
while (nextInstance) {
578
deleteHydratableInstance(fiber, nextInstance);
packages/react-reconciler/src/ReactFiberHydrationContext.old.js
+18
-11
@@ -183,8 +183,7 @@ function deleteHydratableInstance(
183
}
184
}
185
186
-function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
187
- fiber.flags = (fiber.flags & ~Hydrating) | Placement;
186
+function warnNonhydratedInstance(returnFiber: Fiber, fiber: Fiber) {
187
if (__DEV__) {
188
switch (returnFiber.tag) {
189
case HostRoot: {
@@ -283,6 +282,10 @@ function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
282
}
283
}
284
}
285
+function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
286
+ fiber.flags = (fiber.flags & ~Hydrating) | Placement;
287
+ warnNonhydratedInstance(returnFiber, fiber);
288
+}
289
290
function tryHydrate(fiber, nextInstance) {
291
switch (fiber.tag) {
@@ -353,12 +356,10 @@ function shouldClientRenderOnMismatch(fiber: Fiber) {
356
);
357
}
358
356
-function throwOnHydrationMismatchIfConcurrentMode(fiber: Fiber) {
357
- if (shouldClientRenderOnMismatch(fiber)) {
358
- throw new Error(
359
- 'An error occurred during hydration. The server HTML was replaced with client content',
360
- );
361
- }
359
+function throwOnHydrationMismatch(fiber: Fiber) {
360
+ throw new Error(
361
+ 'An error occurred during hydration. The server HTML was replaced with client content',
362
+ );
363
}
364
365
function tryToClaimNextHydratableInstance(fiber: Fiber): void {
@@ -367,7 +368,10 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
368
}
369
let nextInstance = nextHydratableInstance;
370
if (!nextInstance) {
370
- throwOnHydrationMismatchIfConcurrentMode(fiber);
371
+ if (shouldClientRenderOnMismatch(fiber)) {
372
+ warnNonhydratedInstance((hydrationParentFiber: any), fiber);
373
+ throwOnHydrationMismatch(fiber);
374
+ }
375
// Nothing to hydrate. Make it an insertion.
376
insertNonHydratedInstance((hydrationParentFiber: any), fiber);
377
isHydrating = false;
@@ -376,7 +380,10 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
380
}
381
const firstAttemptedInstance = nextInstance;
382
if (!tryHydrate(fiber, nextInstance)) {
379
- throwOnHydrationMismatchIfConcurrentMode(fiber);
383
+ if (shouldClientRenderOnMismatch(fiber)) {
384
+ warnNonhydratedInstance((hydrationParentFiber: any), fiber);
385
+ throwOnHydrationMismatch(fiber);
386
+ }
387
// If we can't hydrate this instance let's try the next one.
388
// We use this as a heuristic. It's based on intuition and not data so it
389
// might be flawed or unnecessary.
@@ -565,7 +572,7 @@ function popHydrationState(fiber: Fiber): boolean {
572
if (nextInstance) {
573
if (shouldClientRenderOnMismatch(fiber)) {
574
warnIfUnhydratedTailNodes(fiber);
568
- throwOnHydrationMismatchIfConcurrentMode(fiber);
575
+ throwOnHydrationMismatch(fiber);
576
} else {
577
while (nextInstance) {
578
deleteHydratableInstance(fiber, nextInstance);