Skip to content

Commit f6aa145

Browse files
committed
Move Hydration Mismatch Errors to Throw or Log Once (Kind of) (#28502)
Stacked on #28476. We used to `console.error` for every mismatch we found, up until the error we threw for the hydration mismatch. This changes it so that we build up a set of diffs up until we either throw or complete hydrating the root/suspense boundary. If we throw, we append the diff to the error message which gets passed to onRecoverableError (which by default is also logged to console). If we complete, we append it to a `console.error`. Since we early abort when something throws, it effectively means that we can only collect multiple diffs if there were preceding non-throwing mismatches - i.e. only properties mismatched but tag name matched. There can still be multiple logs if multiple siblings Suspense boundaries all error hydrating but then they're separate errors entirely. We still log an extra line about something erroring but I think the goal should be that it leads to a single recoverable or console.error. This doesn't yet actually print the diff as part of this message. That's in a follow up PR. DiffTrain build for commit f7aa5e0.
1 parent 4159cc9 commit f6aa145

File tree

4 files changed

+124
-10
lines changed

4 files changed

+124
-10
lines changed

compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react-test-renderer/cjs/ReactTestRenderer-dev.js

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @noflow
88
* @nolint
99
* @preventMunge
10-
* @generated SignedSource<<76f5e861be11fcf494a0e765f9e7dd59>>
10+
* @generated SignedSource<<8afa90ff88a5d63d19c6d736d18ddb41>>
1111
*/
1212

1313
"use strict";
@@ -2669,8 +2669,14 @@ if (__DEV__) {
26692669
}
26702670
}
26712671

2672+
function describeDiff(rootNode) {
2673+
return "\n";
2674+
}
2675+
26722676
var isHydrating = false; // This flag allows for warning supression when we expect there to be mismatches
26732677

2678+
var hydrationDiffRootDEV = null; // Hydration errors that were thrown inside this boundary
2679+
26742680
var hydrationErrors = null;
26752681

26762682
function prepareToHydrateHostInstance(fiber, hostContext) {
@@ -2727,6 +2733,35 @@ if (__DEV__) {
27272733
hydrationErrors.push(error);
27282734
}
27292735
}
2736+
function emitPendingHydrationWarnings() {
2737+
{
2738+
// If we haven't yet thrown any hydration errors by the time we reach the end we've successfully
2739+
// hydrated, however, we might still have DEV-only mismatches that we log now.
2740+
var diffRoot = hydrationDiffRootDEV;
2741+
2742+
if (diffRoot !== null) {
2743+
hydrationDiffRootDEV = null;
2744+
var diff = describeDiff();
2745+
2746+
error(
2747+
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. " +
2748+
"This can happen if a SSR-ed Client Component used:\n" +
2749+
"\n" +
2750+
"- A server/client branch `if (typeof window !== 'undefined')`.\n" +
2751+
"- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
2752+
"- Date formatting in a user's locale which doesn't match the server.\n" +
2753+
"- External changing data without sending a snapshot of it along with the HTML.\n" +
2754+
"- Invalid HTML tag nesting.\n" +
2755+
"\n" +
2756+
"It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n" +
2757+
"\n" +
2758+
"%s%s",
2759+
"https://react.dev/link/hydration-mismatch",
2760+
diff
2761+
);
2762+
}
2763+
}
2764+
}
27302765

27312766
// we wait until the current render is over (either finished or interrupted)
27322767
// before adding it to the fiber/hook queue. Push to this array so we can
@@ -16859,6 +16894,8 @@ if (__DEV__) {
1685916894

1686016895
return false;
1686116896
} else {
16897+
emitPendingHydrationWarnings(); // We might have reentered this boundary to hydrate it. If so, we need to reset the hydration
16898+
1686216899
if ((workInProgress.flags & DidCapture) === NoFlags$1) {
1686316900
// This boundary did not suspend so it's now hydrated and unsuspended.
1686416901
workInProgress.memoizedState = null;
@@ -16962,8 +16999,9 @@ if (__DEV__) {
1696216999
var wasHydrated = popHydrationState();
1696317000

1696417001
if (wasHydrated) {
16965-
// If we hydrated, then we'll need to schedule an update for
17002+
emitPendingHydrationWarnings(); // If we hydrated, then we'll need to schedule an update for
1696617003
// the commit side-effects on the root.
17004+
1696717005
markUpdate(workInProgress);
1696817006
} else {
1696917007
if (current !== null) {
@@ -25482,7 +25520,7 @@ if (__DEV__) {
2548225520
return root;
2548325521
}
2548425522

25485-
var ReactVersion = "19.0.0-canary-57dfa024";
25523+
var ReactVersion = "19.0.0-canary-f7ba3caa";
2548625524

2548725525
// Might add PROFILE later.
2548825526

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
4b8dfd6215bf855402ae1a94cb0ae4f467afca9a
1+
f7aa5e0aa3e2aa51279af4b6cb5413912cacd7f5

compiled-rn/facebook-fbsource/xplat/js/react-native-github/Libraries/Renderer/implementations/ReactFabric-dev.fb.js

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @noflow
88
* @nolint
99
* @preventMunge
10-
* @generated SignedSource<<e76d28cbba11aa5c7a3d0b92eb6ca0f0>>
10+
* @generated SignedSource<<7fb7b6a99b100686fb7a03fec927ee74>>
1111
*/
1212

1313
"use strict";
@@ -6149,8 +6149,14 @@ to return true:wantsResponderID| |
61496149
}
61506150
}
61516151

6152+
function describeDiff(rootNode) {
6153+
return "\n";
6154+
}
6155+
61526156
var isHydrating = false; // This flag allows for warning supression when we expect there to be mismatches
61536157

6158+
var hydrationDiffRootDEV = null; // Hydration errors that were thrown inside this boundary
6159+
61546160
var hydrationErrors = null;
61556161

61566162
function prepareToHydrateHostInstance(fiber, hostContext) {
@@ -6207,6 +6213,35 @@ to return true:wantsResponderID| |
62076213
hydrationErrors.push(error);
62086214
}
62096215
}
6216+
function emitPendingHydrationWarnings() {
6217+
{
6218+
// If we haven't yet thrown any hydration errors by the time we reach the end we've successfully
6219+
// hydrated, however, we might still have DEV-only mismatches that we log now.
6220+
var diffRoot = hydrationDiffRootDEV;
6221+
6222+
if (diffRoot !== null) {
6223+
hydrationDiffRootDEV = null;
6224+
var diff = describeDiff();
6225+
6226+
error(
6227+
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. " +
6228+
"This can happen if a SSR-ed Client Component used:\n" +
6229+
"\n" +
6230+
"- A server/client branch `if (typeof window !== 'undefined')`.\n" +
6231+
"- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
6232+
"- Date formatting in a user's locale which doesn't match the server.\n" +
6233+
"- External changing data without sending a snapshot of it along with the HTML.\n" +
6234+
"- Invalid HTML tag nesting.\n" +
6235+
"\n" +
6236+
"It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n" +
6237+
"\n" +
6238+
"%s%s",
6239+
"https://react.dev/link/hydration-mismatch",
6240+
diff
6241+
);
6242+
}
6243+
}
6244+
}
62106245

62116246
// we wait until the current render is over (either finished or interrupted)
62126247
// before adding it to the fiber/hook queue. Push to this array so we can
@@ -21216,6 +21251,8 @@ to return true:wantsResponderID| |
2121621251

2121721252
return false;
2121821253
} else {
21254+
emitPendingHydrationWarnings(); // We might have reentered this boundary to hydrate it. If so, we need to reset the hydration
21255+
2121921256
if ((workInProgress.flags & DidCapture) === NoFlags$1) {
2122021257
// This boundary did not suspend so it's now hydrated and unsuspended.
2122121258
workInProgress.memoizedState = null;
@@ -21319,8 +21356,9 @@ to return true:wantsResponderID| |
2131921356
var wasHydrated = popHydrationState();
2132021357

2132121358
if (wasHydrated) {
21322-
// If we hydrated, then we'll need to schedule an update for
21359+
emitPendingHydrationWarnings(); // If we hydrated, then we'll need to schedule an update for
2132321360
// the commit side-effects on the root.
21361+
2132421362
markUpdate(workInProgress);
2132521363
} else {
2132621364
if (current !== null) {
@@ -29829,7 +29867,7 @@ to return true:wantsResponderID| |
2982929867
return root;
2983029868
}
2983129869

29832-
var ReactVersion = "19.0.0-canary-1559f120";
29870+
var ReactVersion = "19.0.0-canary-88667624";
2983329871

2983429872
function createPortal$1(
2983529873
children,

compiled-rn/facebook-fbsource/xplat/js/react-native-github/Libraries/Renderer/implementations/ReactNativeRenderer-dev.fb.js

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @noflow
88
* @nolint
99
* @preventMunge
10-
* @generated SignedSource<<aceea7f770f2a0952afa6703fd1f49a6>>
10+
* @generated SignedSource<<86b2303b8da7a666cdb52b8c48916c65>>
1111
*/
1212

1313
"use strict";
@@ -6444,8 +6444,14 @@ to return true:wantsResponderID| |
64446444
}
64456445
}
64466446

6447+
function describeDiff(rootNode) {
6448+
return "\n";
6449+
}
6450+
64476451
var isHydrating = false; // This flag allows for warning supression when we expect there to be mismatches
64486452

6453+
var hydrationDiffRootDEV = null; // Hydration errors that were thrown inside this boundary
6454+
64496455
var hydrationErrors = null;
64506456

64516457
function prepareToHydrateHostInstance(fiber, hostContext) {
@@ -6502,6 +6508,35 @@ to return true:wantsResponderID| |
65026508
hydrationErrors.push(error);
65036509
}
65046510
}
6511+
function emitPendingHydrationWarnings() {
6512+
{
6513+
// If we haven't yet thrown any hydration errors by the time we reach the end we've successfully
6514+
// hydrated, however, we might still have DEV-only mismatches that we log now.
6515+
var diffRoot = hydrationDiffRootDEV;
6516+
6517+
if (diffRoot !== null) {
6518+
hydrationDiffRootDEV = null;
6519+
var diff = describeDiff();
6520+
6521+
error(
6522+
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. " +
6523+
"This can happen if a SSR-ed Client Component used:\n" +
6524+
"\n" +
6525+
"- A server/client branch `if (typeof window !== 'undefined')`.\n" +
6526+
"- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
6527+
"- Date formatting in a user's locale which doesn't match the server.\n" +
6528+
"- External changing data without sending a snapshot of it along with the HTML.\n" +
6529+
"- Invalid HTML tag nesting.\n" +
6530+
"\n" +
6531+
"It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n" +
6532+
"\n" +
6533+
"%s%s",
6534+
"https://react.dev/link/hydration-mismatch",
6535+
diff
6536+
);
6537+
}
6538+
}
6539+
}
65056540

65066541
// we wait until the current render is over (either finished or interrupted)
65076542
// before adding it to the fiber/hook queue. Push to this array so we can
@@ -21254,6 +21289,8 @@ to return true:wantsResponderID| |
2125421289

2125521290
return false;
2125621291
} else {
21292+
emitPendingHydrationWarnings(); // We might have reentered this boundary to hydrate it. If so, we need to reset the hydration
21293+
2125721294
if ((workInProgress.flags & DidCapture) === NoFlags$1) {
2125821295
// This boundary did not suspend so it's now hydrated and unsuspended.
2125921296
workInProgress.memoizedState = null;
@@ -21357,8 +21394,9 @@ to return true:wantsResponderID| |
2135721394
var wasHydrated = popHydrationState();
2135821395

2135921396
if (wasHydrated) {
21360-
// If we hydrated, then we'll need to schedule an update for
21397+
emitPendingHydrationWarnings(); // If we hydrated, then we'll need to schedule an update for
2136121398
// the commit side-effects on the root.
21399+
2136221400
markUpdate(workInProgress);
2136321401
} else {
2136421402
if (current !== null) {
@@ -30269,7 +30307,7 @@ to return true:wantsResponderID| |
3026930307
return root;
3027030308
}
3027130309

30272-
var ReactVersion = "19.0.0-canary-cce7aa34";
30310+
var ReactVersion = "19.0.0-canary-9011959f";
3027330311

3027430312
function createPortal$1(
3027530313
children,

0 commit comments

Comments
 (0)