@samitouri / QOS-React / commits / 1dece52350

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);