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
}