@samitouri / QOS-React / commits / 029e8bd618

Add Owner Stack to attribute hydration mismatches (#32538)

Sebastian "Sebbie" Silbermann committed Mar 6, 2025 at 17:12 UTC 029e8bd618af23fbdd9efdac565ad81f7d4640d8
8 files changed +444 -228
packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js
+51 -53
@@ -201,25 +201,24 @@ describe('ReactDOMFizzForm', () => {
201 await act(async () => {
202 ReactDOMClient.hydrateRoot(container, <App isClient={true} />);
203 });
204 - assertConsoleErrorDev(
205 - [
206 - "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
207 - "This won't be patched up. This can happen if a SSR-ed Client Component used:\n\n" +
208 - "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
209 - "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
210 - "- Date formatting in a user's locale which doesn't match the server.\n" +
211 - '- External changing data without sending a snapshot of it along with the HTML.\n' +
212 - '- Invalid HTML tag nesting.\n\n' +
213 - 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n\n' +
214 - 'https://react.dev/link/hydration-mismatch\n\n' +
215 - ' <App isClient={true}>\n' +
216 - ' <form\n' +
217 - '+ action="action"\n' +
218 - '- action="function"\n' +
219 - ' >\n',
220 - ],
221 - {withoutStack: true},
222 - );
204 + assertConsoleErrorDev([
205 + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
206 + "This won't be patched up. This can happen if a SSR-ed Client Component used:\n\n" +
207 + "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
208 + "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
209 + "- Date formatting in a user's locale which doesn't match the server.\n" +
210 + '- External changing data without sending a snapshot of it along with the HTML.\n' +
211 + '- Invalid HTML tag nesting.\n\n' +
212 + 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n\n' +
213 + 'https://react.dev/link/hydration-mismatch\n\n' +
214 + ' <App isClient={true}>\n' +
215 + ' <form\n' +
216 + '+ action="action"\n' +
217 + '- action="function"\n' +
218 + ' >\n' +
219 + '\n in form (at **)' +
220 + '\n in App (at **)',
221 + ]);
222 });
223
224 it('should ideally warn when passing a string during SSR and function during hydration', async () => {
@@ -392,40 +391,39 @@ describe('ReactDOMFizzForm', () => {
391 await act(async () => {
392 root = ReactDOMClient.hydrateRoot(container, <App />);
393 });
395 - assertConsoleErrorDev(
396 - [
397 - "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
398 - "This won't be patched up. This can happen if a SSR-ed Client Component used:\n\n" +
399 - "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
400 - "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
401 - "- Date formatting in a user's locale which doesn't match the server.\n" +
402 - '- External changing data without sending a snapshot of it along with the HTML.\n' +
403 - '- Invalid HTML tag nesting.\n\n' +
404 - 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n\n' +
405 - 'https://react.dev/link/hydration-mismatch\n\n' +
406 - ' <App>\n' +
407 - ' <form\n' +
408 - ' action={function action}\n' +
409 - ' ref={{current:null}}\n' +
410 - '+ method="DELETE"\n' +
411 - '- method={null}\n' +
412 - ' >\n' +
413 - ' <input\n' +
414 - ' type="submit"\n' +
415 - ' formAction={function action}\n' +
416 - ' ref={{current:null}}\n' +
417 - '+ formTarget="elsewhere"\n' +
418 - '- formTarget={null}\n' +
419 - ' >\n' +
420 - ' <button\n' +
421 - ' formAction={function action}\n' +
422 - ' ref={{current:null}}\n' +
423 - '+ formEncType="text/plain"\n' +
424 - '- formEncType={null}\n' +
425 - ' >\n',
426 - ],
427 - {withoutStack: true},
428 - );
394 + assertConsoleErrorDev([
395 + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
396 + "This won't be patched up. This can happen if a SSR-ed Client Component used:\n\n" +
397 + "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
398 + "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
399 + "- Date formatting in a user's locale which doesn't match the server.\n" +
400 + '- External changing data without sending a snapshot of it along with the HTML.\n' +
401 + '- Invalid HTML tag nesting.\n\n' +
402 + 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n\n' +
403 + 'https://react.dev/link/hydration-mismatch\n\n' +
404 + ' <App>\n' +
405 + ' <form\n' +
406 + ' action={function action}\n' +
407 + ' ref={{current:null}}\n' +
408 + '+ method="DELETE"\n' +
409 + '- method={null}\n' +
410 + ' >\n' +
411 + ' <input\n' +
412 + ' type="submit"\n' +
413 + ' formAction={function action}\n' +
414 + ' ref={{current:null}}\n' +
415 + '+ formTarget="elsewhere"\n' +
416 + '- formTarget={null}\n' +
417 + ' >\n' +
418 + ' <button\n' +
419 + ' formAction={function action}\n' +
420 + ' ref={{current:null}}\n' +
421 + '+ formEncType="text/plain"\n' +
422 + '- formEncType={null}\n' +
423 + ' >\n' +
424 + '\n in input (at **)' +
425 + '\n in App (at **)',
426 + ]);
427 await act(async () => {
428 root.render(<App isUpdate={true} />);
429 });
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+1 -1
@@ -10233,7 +10233,7 @@ describe('ReactDOMFizzServer', () => {
10233 '\n+ client' +
10234 '\n- server' +
10235 '\n' +
10236 - '\n in Suspense (at **)' +
10236 + '\n in meta (at **)' +
10237 '\n in ClientApp (at **)',
10238 ]);
10239 }
packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js
+263 -50
@@ -23,6 +23,7 @@ function errorHandler() {
23
24 describe('ReactDOMServerHydration', () => {
25 let container;
26 + let ownerStacks;
27
28 beforeEach(() => {
29 jest.resetModules();
@@ -32,7 +33,15 @@ describe('ReactDOMServerHydration', () => {
33 act = React.act;
34
35 window.addEventListener('error', errorHandler);
35 - console.error = jest.fn();
36 + ownerStacks = [];
37 + console.error = jest.fn(() => {
38 + const ownerStack = React.captureOwnerStack();
39 + if (typeof ownerStack === 'string') {
40 + ownerStacks.push(ownerStack === '' ? ' <empty>' : ownerStack);
41 + } else {
42 + ownerStacks.push(' ' + String(ownerStack));
43 + }
44 + });
45 container = document.createElement('div');
46 document.body.appendChild(container);
47 });
@@ -44,15 +53,25 @@ describe('ReactDOMServerHydration', () => {
53 });
54
55 function normalizeCodeLocInfo(str) {
47 - return (
48 - typeof str === 'string' &&
49 - str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) {
50 - return '\n in ' + name + ' (at **)';
51 - })
52 - );
56 + return typeof str === 'string'
57 + ? str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) {
58 + return '\n in ' + name + ' (at **)';
59 + })
60 + : str;
61 }
62
55 - function formatMessage(args) {
63 + function formatMessage(args, index) {
64 + const ownerStack = ownerStacks[index];
65 +
66 + if (ownerStack === undefined) {
67 + throw new Error(
68 + 'Expected an owner stack for message ' +
69 + index +
70 + ':\n' +
71 + util.format(...args),
72 + );
73 + }
74 +
75 const [format, ...rest] = args;
76 if (format instanceof Error) {
77 if (format.cause instanceof Error) {
@@ -61,13 +80,23 @@ describe('ReactDOMServerHydration', () => {
80 format.message +
81 ']\n Cause [' +
82 format.cause.message +
64 - ']'
83 + ']\n Owner Stack:' +
84 + normalizeCodeLocInfo(ownerStack)
85 );
86 }
67 - return 'Caught [' + format.message + ']';
87 + return (
88 + 'Caught [' +
89 + format.message +
90 + ']\n Owner Stack:' +
91 + normalizeCodeLocInfo(ownerStack)
92 + );
93 }
94 rest[rest.length - 1] = normalizeCodeLocInfo(rest[rest.length - 1]);
70 - return util.format(format, ...rest);
95 + return (
96 + util.format(format, ...rest) +
97 + '\n Owner Stack:' +
98 + normalizeCodeLocInfo(ownerStack)
99 + );
100 }
101
102 function formatConsoleErrors() {
@@ -115,7 +144,10 @@ describe('ReactDOMServerHydration', () => {
144 <main className="child">
145 + client
146 - server
118 - ]",
147 + ]
148 + Owner Stack:
149 + in main (at **)
150 + in Mismatch (at **)",
151 ]
152 `);
153 } else {
@@ -138,7 +170,10 @@ describe('ReactDOMServerHydration', () => {
170 <main className="child">
171 + client
172 - server
141 - ",
173 +
174 + Owner Stack:
175 + in main (at **)
176 + in Mismatch (at **)",
177 ]
178 `);
179 }
@@ -177,7 +212,10 @@ describe('ReactDOMServerHydration', () => {
212 <div>
213 + This markup contains an nbsp entity:   client text
214 - This markup contains an nbsp entity:   server text
180 - ]",
215 + ]
216 + Owner Stack:
217 + in div (at **)
218 + in Mismatch (at **)",
219 ]
220 `);
221 } else {
@@ -199,7 +237,10 @@ describe('ReactDOMServerHydration', () => {
237 <div>
238 + This markup contains an nbsp entity:   client text
239 - This markup contains an nbsp entity:   server text
202 - ",
240 +
241 + Owner Stack:
242 + in div (at **)
243 + in Mismatch (at **)",
244 ]
245 `);
246 }
@@ -245,7 +286,10 @@ describe('ReactDOMServerHydration', () => {
286 - __html: "<span>server</span>"
287 }}
288 >
248 - ",
289 +
290 + Owner Stack:
291 + in main (at **)
292 + in Mismatch (at **)",
293 ]
294 `);
295 });
@@ -286,7 +330,10 @@ describe('ReactDOMServerHydration', () => {
330 + dir="ltr"
331 - dir="rtl"
332 >
289 - ",
333 +
334 + Owner Stack:
335 + in main (at **)
336 + in Mismatch (at **)",
337 ]
338 `);
339 });
@@ -327,7 +374,10 @@ describe('ReactDOMServerHydration', () => {
374 + dir="ltr"
375 - dir={null}
376 >
330 - ",
377 +
378 + Owner Stack:
379 + in main (at **)
380 + in Mismatch (at **)",
381 ]
382 `);
383 });
@@ -368,7 +418,10 @@ describe('ReactDOMServerHydration', () => {
418 + dir={null}
419 - dir="rtl"
420 >
371 - ",
421 +
422 + Owner Stack:
423 + in main (at **)
424 + in Mismatch (at **)",
425 ]
426 `);
427 });
@@ -409,7 +462,10 @@ describe('ReactDOMServerHydration', () => {
462 + dir={null}
463 - dir="rtl"
464 >
412 - ",
465 +
466 + Owner Stack:
467 + in main (at **)
468 + in Mismatch (at **)",
469 ]
470 `);
471 });
@@ -449,7 +505,78 @@ describe('ReactDOMServerHydration', () => {
505 + style={{opacity:1}}
506 - style={{opacity:"0"}}
507 >
452 - ",
508 +
509 + Owner Stack:
510 + in main (at **)
511 + in Mismatch (at **)",
512 + ]
513 + `);
514 + });
515 +
516 + // @gate __DEV__
517 + it('picks the DFS-first Fiber as the error Owner', () => {
518 + function LeftMismatch({isClient}) {
519 + return <div className={isClient ? 'client' : 'server'} />;
520 + }
521 +
522 + function LeftIndirection({isClient}) {
523 + return <LeftMismatch isClient={isClient} />;
524 + }
525 +
526 + function MiddleMismatch({isClient}) {
527 + return <span className={isClient ? 'client' : 'server'} />;
528 + }
529 +
530 + function RightMisMatch({isClient}) {
531 + return <p className={isClient ? 'client' : 'server'} />;
532 + }
533 +
534 + function App({isClient}) {
535 + return (
536 + <>
537 + <LeftIndirection isClient={isClient} />
538 + <MiddleMismatch isClient={isClient} />
539 + <RightMisMatch isClient={isClient} />
540 + </>
541 + );
542 + }
543 + expect(testMismatch(App)).toMatchInlineSnapshot(`
544 + [
545 + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:
546 +
547 + - A server/client branch \`if (typeof window !== 'undefined')\`.
548 + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called.
549 + - Date formatting in a user's locale which doesn't match the server.
550 + - External changing data without sending a snapshot of it along with the HTML.
551 + - Invalid HTML tag nesting.
552 +
553 + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
554 +
555 + https://react.dev/link/hydration-mismatch
556 +
557 + <App isClient={true}>
558 + <LeftIndirection isClient={true}>
559 + <LeftMismatch isClient={true}>
560 + <div
561 + + className="client"
562 + - className="server"
563 + >
564 + <MiddleMismatch isClient={true}>
565 + <span
566 + + className="client"
567 + - className="server"
568 + >
569 + <RightMisMatch isClient={true}>
570 + <p
571 + + className="client"
572 + - className="server"
573 + >
574 +
575 + Owner Stack:
576 + in div (at **)
577 + in LeftMismatch (at **)
578 + in LeftIndirection (at **)
579 + in App (at **)",
580 ]
581 `);
582 });
@@ -483,7 +610,10 @@ describe('ReactDOMServerHydration', () => {
610 <Mismatch isClient={true}>
611 <div className="parent">
612 + <main className="only">
486 - ]",
613 + ]
614 + Owner Stack:
615 + in main (at **)
616 + in Mismatch (at **)",
617 ]
618 `);
619 });
@@ -518,7 +648,10 @@ describe('ReactDOMServerHydration', () => {
648 + <header className="1">
649 - <main className="2">
650 ...
521 - ]",
651 + ]
652 + Owner Stack:
653 + in header (at **)
654 + in Mismatch (at **)",
655 ]
656 `);
657 });
@@ -554,7 +687,10 @@ describe('ReactDOMServerHydration', () => {
687 + <main className="2">
688 - <footer className="3">
689 ...
557 - ]",
690 + ]
691 + Owner Stack:
692 + in main (at **)
693 + in Mismatch (at **)",
694 ]
695 `);
696 });
@@ -589,7 +725,10 @@ describe('ReactDOMServerHydration', () => {
725 <header>
726 <main>
727 + <footer className="3">
592 - ]",
728 + ]
729 + Owner Stack:
730 + in footer (at **)
731 + in Mismatch (at **)",
732 ]
733 `);
734 });
@@ -620,7 +759,10 @@ describe('ReactDOMServerHydration', () => {
759 <div className="parent">
760 + only
761 -
623 - ]",
762 + ]
763 + Owner Stack:
764 + in div (at **)
765 + in Mismatch (at **)",
766 ]
767 `);
768 } else {
@@ -642,7 +784,10 @@ describe('ReactDOMServerHydration', () => {
784 <div className="parent">
785 + only
786 -
645 - ",
787 +
788 + Owner Stack:
789 + in div (at **)
790 + in Mismatch (at **)",
791 ]
792 `);
793 }
@@ -679,7 +824,10 @@ describe('ReactDOMServerHydration', () => {
824 + second
825 - <footer className="3">
826 ...
682 - ]",
827 + ]
828 + Owner Stack:
829 + in div (at **)
830 + in Mismatch (at **)",
831 ]
832 `);
833 });
@@ -714,7 +862,10 @@ describe('ReactDOMServerHydration', () => {
862 + first
863 - <main className="2">
864 ...
717 - ]",
865 + ]
866 + Owner Stack:
867 + in div (at **)
868 + in Mismatch (at **)",
869 ]
870 `);
871 });
@@ -749,7 +900,10 @@ describe('ReactDOMServerHydration', () => {
900 <header>
901 <main>
902 + third
752 - ]",
903 + ]
904 + Owner Stack:
905 + in div (at **)
906 + in Mismatch (at **)",
907 ]
908 `);
909 });
@@ -784,7 +938,10 @@ describe('ReactDOMServerHydration', () => {
938 <Mismatch isClient={true}>
939 <div className="parent">
940 - <main className="only">
787 - ]",
941 + ]
942 + Owner Stack:
943 + in div (at **)
944 + in Mismatch (at **)",
945 ]
946 `);
947 });
@@ -819,7 +976,10 @@ describe('ReactDOMServerHydration', () => {
976 + <main className="2">
977 - <header className="1">
978 ...
822 - ]",
979 + ]
980 + Owner Stack:
981 + in main (at **)
982 + in Mismatch (at **)",
983 ]
984 `);
985 });
@@ -854,7 +1014,10 @@ describe('ReactDOMServerHydration', () => {
1014 <header>
1015 + <footer className="3">
1016 - <main className="2">
857 - ]",
1017 + ]
1018 + Owner Stack:
1019 + in footer (at **)
1020 + in Mismatch (at **)",
1021 ]
1022 `);
1023 });
@@ -887,7 +1050,10 @@ describe('ReactDOMServerHydration', () => {
1050 <Mismatch isClient={true}>
1051 <div className="parent">
1052 - <footer className="3">
890 - ]",
1053 + ]
1054 + Owner Stack:
1055 + in div (at **)
1056 + in Mismatch (at **)",
1057 ]
1058 `);
1059 });
@@ -916,7 +1082,10 @@ describe('ReactDOMServerHydration', () => {
1082 <Mismatch isClient={true}>
1083 <div className="parent">
1084 - only
919 - ]",
1085 + ]
1086 + Owner Stack:
1087 + in div (at **)
1088 + in Mismatch (at **)",
1089 ]
1090 `);
1091 });
@@ -951,7 +1120,10 @@ describe('ReactDOMServerHydration', () => {
1120 + <main className="2">
1121 - first
1122 ...
954 - ]",
1123 + ]
1124 + Owner Stack:
1125 + in main (at **)
1126 + in Mismatch (at **)",
1127 ]
1128 `);
1129 });
@@ -986,7 +1158,10 @@ describe('ReactDOMServerHydration', () => {
1158 <header>
1159 + <footer className="3">
1160 - second
989 - ]",
1161 + ]
1162 + Owner Stack:
1163 + in footer (at **)
1164 + in Mismatch (at **)",
1165 ]
1166 `);
1167 });
@@ -1019,7 +1194,10 @@ describe('ReactDOMServerHydration', () => {
1194 <Mismatch isClient={true}>
1195 <div className="parent">
1196 - third
1022 - ]",
1197 + ]
1198 + Owner Stack:
1199 + in div (at **)
1200 + in Mismatch (at **)",
1201 ]
1202 `);
1203 });
@@ -1062,7 +1240,10 @@ describe('ReactDOMServerHydration', () => {
1240 <Mismatch isClient={true}>
1241 <div className="parent">
1242 + <Suspense fallback={<p>}>
1065 - ]",
1243 + ]
1244 + Owner Stack:
1245 + in Suspense (at **)
1246 + in Mismatch (at **)",
1247 ]
1248 `);
1249 });
@@ -1097,7 +1278,10 @@ describe('ReactDOMServerHydration', () => {
1278 <Mismatch isClient={true}>
1279 <div className="parent">
1280 - <Suspense>
1100 - ]",
1281 + ]
1282 + Owner Stack:
1283 + in div (at **)
1284 + in Mismatch (at **)",
1285 ]
1286 `);
1287 });
@@ -1134,7 +1318,10 @@ describe('ReactDOMServerHydration', () => {
1318 <Mismatch isClient={true}>
1319 <div className="parent">
1320 + <Suspense fallback={<p>}>
1137 - ]",
1321 + ]
1322 + Owner Stack:
1323 + in Suspense (at **)
1324 + in Mismatch (at **)",
1325 ]
1326 `);
1327 });
@@ -1175,7 +1362,10 @@ describe('ReactDOMServerHydration', () => {
1362 <Mismatch isClient={true}>
1363 <div className="parent">
1364 - <Suspense>
1178 - ]",
1365 + ]
1366 + Owner Stack:
1367 + in div (at **)
1368 + in Mismatch (at **)",
1369 ]
1370 `);
1371 });
@@ -1214,7 +1404,10 @@ describe('ReactDOMServerHydration', () => {
1404 + <main className="second">
1405 - <footer className="3">
1406 ...
1217 - ]",
1407 + ]
1408 + Owner Stack:
1409 + in main (at **)
1410 + in Mismatch (at **)",
1411 ]
1412 `);
1413 });
@@ -1252,7 +1445,10 @@ describe('ReactDOMServerHydration', () => {
1445 <header>
1446 + <footer className="3">
1447 - <main className="second">
1255 - ]",
1448 + ]
1449 + Owner Stack:
1450 + in footer (at **)
1451 + in Mismatch (at **)",
1452 ]
1453 `);
1454 });
@@ -1280,7 +1476,8 @@ describe('ReactDOMServerHydration', () => {
1476 [
1477 "Caught [Switched to client rendering because the server rendering aborted due to:
1478
1283 - The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToPipeableStream" which supports Suspense on the server]",
1479 + The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToPipeableStream" which supports Suspense on the server]
1480 + Owner Stack: null",
1481 ]
1482 `);
1483 });
@@ -1308,7 +1505,8 @@ describe('ReactDOMServerHydration', () => {
1505 [
1506 "Caught [Switched to client rendering because the server rendering aborted due to:
1507
1311 - The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToPipeableStream" which supports Suspense on the server]",
1508 + The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToPipeableStream" which supports Suspense on the server]
1509 + Owner Stack: null",
1510 ]
1511 `);
1512 });
@@ -1348,7 +1546,10 @@ describe('ReactDOMServerHydration', () => {
1546 <div className="parent">
1547 + <header className="1">
1548 ...
1351 - ]",
1549 + ]
1550 + Owner Stack:
1551 + in header (at **)
1552 + in Mismatch (at **)",
1553 ]
1554 `);
1555 });
@@ -1387,7 +1588,10 @@ describe('ReactDOMServerHydration', () => {
1588 - <header className="1">
1589 - <main className="2">
1590 - <footer className="3">
1390 - ]",
1591 + ]
1592 + Owner Stack:
1593 + in div (at **)
1594 + in Mismatch (at **)",
1595 ]
1596 `);
1597 });
@@ -1451,7 +1655,12 @@ describe('ReactDOMServerHydration', () => {
1655 <header>
1656 <main>
1657 + <footer className="3">
1454 - ]",
1658 + ]
1659 + Owner Stack:
1660 + in footer (at **)
1661 + in Panel (at **)
1662 + in ProfileSettings (at **)
1663 + in Mismatch (at **)",
1664 ]
1665 `);
1666 });
@@ -1508,7 +1717,11 @@ describe('ReactDOMServerHydration', () => {
1717 <ProfileSettings>
1718 <div className="parent">
1719 - <footer className="3">
1511 - ]",
1720 + ]
1721 + Owner Stack:
1722 + in div (at **)
1723 + in ProfileSettings (at **)
1724 + in Mismatch (at **)",
1725 ]
1726 `);
1727 });
packages/react-dom/src/__tests__/ReactDOMRoot-test.js
+20 -22
@@ -164,28 +164,26 @@ describe('ReactDOMRoot', () => {
164 </div>,
165 );
166 await waitForAll([]);
167 - assertConsoleErrorDev(
168 - [
169 - "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
170 - "This won't be patched up. This can happen if a SSR-ed Client Component used:\n" +
171 - '\n' +
172 - "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
173 - "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
174 - "- Date formatting in a user's locale which doesn't match the server.\n" +
175 - '- External changing data without sending a snapshot of it along with the HTML.\n' +
176 - '- Invalid HTML tag nesting.\n' +
177 - '\n' +
178 - 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n' +
179 - '\n' +
180 - 'https://react.dev/link/hydration-mismatch\n' +
181 - '\n' +
182 - ' <div>\n' +
183 - ' <span\n' +
184 - '- className="extra"\n' +
185 - ' >\n',
186 - ],
187 - {withoutStack: true},
188 - );
167 + assertConsoleErrorDev([
168 + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
169 + "This won't be patched up. This can happen if a SSR-ed Client Component used:\n" +
170 + '\n' +
171 + "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
172 + "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
173 + "- Date formatting in a user's locale which doesn't match the server.\n" +
174 + '- External changing data without sending a snapshot of it along with the HTML.\n' +
175 + '- Invalid HTML tag nesting.\n' +
176 + '\n' +
177 + 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n' +
178 + '\n' +
179 + 'https://react.dev/link/hydration-mismatch\n' +
180 + '\n' +
181 + ' <div>\n' +
182 + ' <span\n' +
183 + '- className="extra"\n' +
184 + ' >\n' +
185 + '\n in span (at **)',
186 + ]);
187 });
188
189 it('clears existing children', async () => {
packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js
+24 -26
@@ -547,32 +547,30 @@ describe('ReactDOM HostSingleton', () => {
547 );
548 expect(hydrationErrors).toEqual([]);
549 await waitForAll([]);
550 - assertConsoleErrorDev(
551 - [
552 - "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
553 - "This won't be patched up. This can happen if a SSR-ed Client Component used:\n" +
554 - '\n' +
555 - "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
556 - "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
557 - "- Date formatting in a user's locale which doesn't match the server.\n" +
558 - '- External changing data without sending a snapshot of it along with the HTML.\n' +
559 - '- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension installed ' +
560 - 'which messes with the HTML before React loaded.\n' +
561 - '\n' +
562 - 'https://react.dev/link/hydration-mismatch\n' +
563 - '\n' +
564 - ' <html\n' +
565 - '+ data-client-foo="foo"\n' +
566 - '- data-client-foo={null}\n' +
567 - ' >\n' +
568 - ' <head>\n' +
569 - ' <body\n' +
570 - '+ data-client-baz="baz"\n' +
571 - '- data-client-baz={null}\n' +
572 - ' >\n',
573 - ],
574 - {withoutStack: true},
575 - );
550 + assertConsoleErrorDev([
551 + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
552 + "This won't be patched up. This can happen if a SSR-ed Client Component used:\n" +
553 + '\n' +
554 + "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
555 + "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
556 + "- Date formatting in a user's locale which doesn't match the server.\n" +
557 + '- External changing data without sending a snapshot of it along with the HTML.\n' +
558 + '- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension installed ' +
559 + 'which messes with the HTML before React loaded.\n' +
560 + '\n' +
561 + 'https://react.dev/link/hydration-mismatch\n' +
562 + '\n' +
563 + ' <html\n' +
564 + '+ data-client-foo="foo"\n' +
565 + '- data-client-foo={null}\n' +
566 + ' >\n' +
567 + ' <head>\n' +
568 + ' <body\n' +
569 + '+ data-client-baz="baz"\n' +
570 + '- data-client-baz={null}\n' +
571 + ' >\n' +
572 + '\n in body (at **)',
573 + ]);
574 expect(persistentElements).toEqual([
575 document.documentElement,
576 document.head,
packages/react-dom/src/__tests__/ReactRenderDocument-test.js
+3 -2
@@ -311,9 +311,10 @@ describe('rendering React components at document', () => {
311 '+ Hello world\n' +
312 '- Goodbye world\n' +
313 '+ Hello world\n' +
314 - '- Goodbye world\n',
314 + '- Goodbye world\n' +
315 + '\n in body (at **)' +
316 + '\n in Component (at **)',
317 ],
316 - {withoutStack: true},
318 );
319
320 assertLog(
packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
+55 -58
@@ -163,9 +163,10 @@ describe('ReactDOMServerHydration', () => {
163 ' <TestComponent name="y" ref={function ref}>\n' +
164 ' <span ref={{current:null}} onClick={function}>\n' +
165 '+ y\n' +
166 - '- x\n',
166 + '- x\n' +
167 + '\n in span (at **)' +
168 + '\n in TestComponent (at **)',
169 ],
168 - {withoutStack: true},
170 );
171 expect(mountCount).toEqual(4);
172 expect(element.innerHTML.length > 0).toBe(true);
@@ -269,9 +270,9 @@ describe('ReactDOMServerHydration', () => {
270 '\n' +
271 ' <button autoFocus={false} onFocus={function mockConstructor}>\n' +
272 '+ client\n' +
272 - '- server\n',
273 + '- server\n' +
274 + '\n in button (at **)',
275 ],
274 - {withoutStack: true},
276 );
277
278 expect(onFocusBeforeHydration).not.toHaveBeenCalled();
@@ -294,31 +295,29 @@ describe('ReactDOMServerHydration', () => {
295 />,
296 );
297 });
297 - assertConsoleErrorDev(
298 - [
299 - "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
300 - "This won't be patched up. This can happen if a SSR-ed Client Component used:\n" +
301 - '\n' +
302 - "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
303 - "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
304 - "- Date formatting in a user's locale which doesn't match the server.\n" +
305 - '- External changing data without sending a snapshot of it along with the HTML.\n' +
306 - '- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension ' +
307 - 'installed which messes with the HTML before React loaded.\n' +
308 - '\n' +
309 - 'https://react.dev/link/hydration-mismatch\n' +
310 - '\n' +
311 - ' <div\n style={{\n+ textDecoration: "none"\n' +
312 - '+ color: "white"\n' +
313 - '- color: "black"\n' +
314 - '+ height: "10px"\n' +
315 - '- height: "10px"\n' +
316 - '- text-decoration: "none"\n' +
317 - ' }}\n' +
318 - ' >\n',
319 - ],
320 - {withoutStack: true},
321 - );
298 + assertConsoleErrorDev([
299 + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
300 + "This won't be patched up. This can happen if a SSR-ed Client Component used:\n" +
301 + '\n' +
302 + "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
303 + "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
304 + "- Date formatting in a user's locale which doesn't match the server.\n" +
305 + '- External changing data without sending a snapshot of it along with the HTML.\n' +
306 + '- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension ' +
307 + 'installed which messes with the HTML before React loaded.\n' +
308 + '\n' +
309 + 'https://react.dev/link/hydration-mismatch\n' +
310 + '\n' +
311 + ' <div\n style={{\n+ textDecoration: "none"\n' +
312 + '+ color: "white"\n' +
313 + '- color: "black"\n' +
314 + '+ height: "10px"\n' +
315 + '- height: "10px"\n' +
316 + '- text-decoration: "none"\n' +
317 + ' }}\n' +
318 + ' >\n' +
319 + '\n in div (at **)',
320 + ]);
321 });
322
323 it('should not warn when the style property differs on whitespace or order in IE', async () => {
@@ -362,33 +361,31 @@ describe('ReactDOMServerHydration', () => {
361 />,
362 );
363 });
365 - assertConsoleErrorDev(
366 - [
367 - "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
368 - "This won't be patched up. This can happen if a SSR-ed Client Component used:\n" +
369 - '\n' +
370 - "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
371 - "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
372 - "- Date formatting in a user's locale which doesn't match the server.\n" +
373 - '- External changing data without sending a snapshot of it along with the HTML.\n' +
374 - '- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension ' +
375 - 'installed which messes with the HTML before React loaded.\n' +
376 - '\n' +
377 - 'https://react.dev/link/hydration-mismatch\n' +
378 - '\n' +
379 - ' <div\n' +
380 - ' style={{\n' +
381 - '+ textDecoration: "none"\n' +
382 - '+ color: "black"\n' +
383 - '- color: "black"\n' +
384 - '+ height: "10px"\n' +
385 - '- height: "10px"\n' +
386 - '- text-decoration: "none"\n' +
387 - ' }}\n' +
388 - ' >\n',
389 - ],
390 - {withoutStack: true},
391 - );
364 + assertConsoleErrorDev([
365 + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " +
366 + "This won't be patched up. This can happen if a SSR-ed Client Component used:\n" +
367 + '\n' +
368 + "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
369 + "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
370 + "- Date formatting in a user's locale which doesn't match the server.\n" +
371 + '- External changing data without sending a snapshot of it along with the HTML.\n' +
372 + '- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension ' +
373 + 'installed which messes with the HTML before React loaded.\n' +
374 + '\n' +
375 + 'https://react.dev/link/hydration-mismatch\n' +
376 + '\n' +
377 + ' <div\n' +
378 + ' style={{\n' +
379 + '+ textDecoration: "none"\n' +
380 + '+ color: "black"\n' +
381 + '- color: "black"\n' +
382 + '+ height: "10px"\n' +
383 + '- height: "10px"\n' +
384 + '- text-decoration: "none"\n' +
385 + ' }}\n' +
386 + ' >\n' +
387 + '\n in div (at **)',
388 + ]);
389 });
390
391 it('should throw rendering portals on the server', () => {
@@ -652,9 +649,9 @@ describe('ReactDOMServerHydration', () => {
649 ' <div dangerouslySetInnerHTML={undefined}>\n' +
650 ' <p>\n' +
651 '+ client\n' +
655 - '- server\n',
652 + '- server\n' +
653 + '\n in p (at **)',
654 ],
657 - {withoutStack: true},
655 );
656
657 if (favorSafetyOverHydrationPerf) {
packages/react-reconciler/src/ReactFiberHydrationContext.js
+27 -16
@@ -67,6 +67,7 @@ import {
67 import {queueRecoverableErrors} from './ReactFiberWorkLoop';
68 import {getRootHostContainer, getHostContext} from './ReactFiberHostContext';
69 import {describeDiff} from './ReactFiberHydrationDiffs';
70 +import {runWithFiberInDEV} from './ReactCurrentFiber';
71
72 // The deepest Fiber on the stack involved in a hydration context.
73 // This may have been an insertion or a hydration.
@@ -749,22 +750,32 @@ export function emitPendingHydrationWarnings() {
750 if (diffRoot !== null) {
751 hydrationDiffRootDEV = null;
752 const diff = describeDiff(diffRoot);
752 - console.error(
753 - "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. " +
754 - 'This can happen if a SSR-ed Client Component used:\n' +
755 - '\n' +
756 - "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
757 - "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
758 - "- Date formatting in a user's locale which doesn't match the server.\n" +
759 - '- External changing data without sending a snapshot of it along with the HTML.\n' +
760 - '- Invalid HTML tag nesting.\n' +
761 - '\n' +
762 - 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n' +
763 - '\n' +
764 - '%s%s',
765 - 'https://react.dev/link/hydration-mismatch',
766 - diff,
767 - );
753 +
754 + // Just pick the DFS-first leaf as the owner.
755 + // Should be good enough since most warnings only have a single error.
756 + let diffOwner: HydrationDiffNode = diffRoot;
757 + while (diffOwner.children.length > 0) {
758 + diffOwner = diffOwner.children[0];
759 + }
760 +
761 + runWithFiberInDEV(diffOwner.fiber, () => {
762 + console.error(
763 + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. " +
764 + 'This can happen if a SSR-ed Client Component used:\n' +
765 + '\n' +
766 + "- A server/client branch `if (typeof window !== 'undefined')`.\n" +
767 + "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
768 + "- Date formatting in a user's locale which doesn't match the server.\n" +
769 + '- External changing data without sending a snapshot of it along with the HTML.\n' +
770 + '- Invalid HTML tag nesting.\n' +
771 + '\n' +
772 + 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n' +
773 + '\n' +
774 + '%s%s',
775 + 'https://react.dev/link/hydration-mismatch',
776 + diff,
777 + );
778 + });
779 }
780 }
781 }