Skip to content

Commit 5add535

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 [f7aa5e0](f7aa5e0)
1 parent 0602001 commit 5add535

16 files changed

+930
-1308
lines changed

compiled/facebook-www/REVISION

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

compiled/facebook-www/ReactART-dev.classic.js

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ if (__DEV__) {
6666
return self;
6767
}
6868

69-
var ReactVersion = "19.0.0-www-classic-9130e73d";
69+
var ReactVersion = "19.0.0-www-classic-6c991e9f";
7070

7171
var LegacyRoot = 0;
7272
var ConcurrentRoot = 1;
@@ -3719,8 +3719,14 @@ if (__DEV__) {
37193719
}
37203720
}
37213721

3722+
function describeDiff(rootNode) {
3723+
return "\n";
3724+
}
3725+
37223726
var isHydrating = false; // This flag allows for warning supression when we expect there to be mismatches
37233727

3728+
var hydrationDiffRootDEV = null; // Hydration errors that were thrown inside this boundary
3729+
37243730
var hydrationErrors = null;
37253731

37263732
function prepareToHydrateHostInstance(fiber, hostContext) {
@@ -3777,6 +3783,35 @@ if (__DEV__) {
37773783
hydrationErrors.push(error);
37783784
}
37793785
}
3786+
function emitPendingHydrationWarnings() {
3787+
{
3788+
// If we haven't yet thrown any hydration errors by the time we reach the end we've successfully
3789+
// hydrated, however, we might still have DEV-only mismatches that we log now.
3790+
var diffRoot = hydrationDiffRootDEV;
3791+
3792+
if (diffRoot !== null) {
3793+
hydrationDiffRootDEV = null;
3794+
var diff = describeDiff();
3795+
3796+
error(
3797+
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. " +
3798+
"This can happen if a SSR-ed Client Component used:\n" +
3799+
"\n" +
3800+
"- A server/client branch `if (typeof window !== 'undefined')`.\n" +
3801+
"- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
3802+
"- Date formatting in a user's locale which doesn't match the server.\n" +
3803+
"- External changing data without sending a snapshot of it along with the HTML.\n" +
3804+
"- Invalid HTML tag nesting.\n" +
3805+
"\n" +
3806+
"It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n" +
3807+
"\n" +
3808+
"%s%s",
3809+
"https://react.dev/link/hydration-mismatch",
3810+
diff
3811+
);
3812+
}
3813+
}
3814+
}
37803815

37813816
// we wait until the current render is over (either finished or interrupted)
37823817
// before adding it to the fiber/hook queue. Push to this array so we can
@@ -19785,6 +19820,8 @@ if (__DEV__) {
1978519820

1978619821
return false;
1978719822
} else {
19823+
emitPendingHydrationWarnings(); // We might have reentered this boundary to hydrate it. If so, we need to reset the hydration
19824+
1978819825
if ((workInProgress.flags & DidCapture) === NoFlags$1) {
1978919826
// This boundary did not suspend so it's now hydrated and unsuspended.
1979019827
workInProgress.memoizedState = null;
@@ -19904,8 +19941,9 @@ if (__DEV__) {
1990419941
var wasHydrated = popHydrationState();
1990519942

1990619943
if (wasHydrated) {
19907-
// If we hydrated, then we'll need to schedule an update for
19944+
emitPendingHydrationWarnings(); // If we hydrated, then we'll need to schedule an update for
1990819945
// the commit side-effects on the root.
19946+
1990919947
markUpdate(workInProgress);
1991019948
} else {
1991119949
if (current !== null) {

compiled/facebook-www/ReactART-dev.modern.js

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ if (__DEV__) {
6666
return self;
6767
}
6868

69-
var ReactVersion = "19.0.0-www-modern-937bdc18";
69+
var ReactVersion = "19.0.0-www-modern-515161fd";
7070

7171
var LegacyRoot = 0;
7272
var ConcurrentRoot = 1;
@@ -3484,8 +3484,14 @@ if (__DEV__) {
34843484
}
34853485
}
34863486

3487+
function describeDiff(rootNode) {
3488+
return "\n";
3489+
}
3490+
34873491
var isHydrating = false; // This flag allows for warning supression when we expect there to be mismatches
34883492

3493+
var hydrationDiffRootDEV = null; // Hydration errors that were thrown inside this boundary
3494+
34893495
var hydrationErrors = null;
34903496

34913497
function prepareToHydrateHostInstance(fiber, hostContext) {
@@ -3542,6 +3548,35 @@ if (__DEV__) {
35423548
hydrationErrors.push(error);
35433549
}
35443550
}
3551+
function emitPendingHydrationWarnings() {
3552+
{
3553+
// If we haven't yet thrown any hydration errors by the time we reach the end we've successfully
3554+
// hydrated, however, we might still have DEV-only mismatches that we log now.
3555+
var diffRoot = hydrationDiffRootDEV;
3556+
3557+
if (diffRoot !== null) {
3558+
hydrationDiffRootDEV = null;
3559+
var diff = describeDiff();
3560+
3561+
error(
3562+
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. " +
3563+
"This can happen if a SSR-ed Client Component used:\n" +
3564+
"\n" +
3565+
"- A server/client branch `if (typeof window !== 'undefined')`.\n" +
3566+
"- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
3567+
"- Date formatting in a user's locale which doesn't match the server.\n" +
3568+
"- External changing data without sending a snapshot of it along with the HTML.\n" +
3569+
"- Invalid HTML tag nesting.\n" +
3570+
"\n" +
3571+
"It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n" +
3572+
"\n" +
3573+
"%s%s",
3574+
"https://react.dev/link/hydration-mismatch",
3575+
diff
3576+
);
3577+
}
3578+
}
3579+
}
35453580

35463581
// we wait until the current render is over (either finished or interrupted)
35473582
// before adding it to the fiber/hook queue. Push to this array so we can
@@ -19473,6 +19508,8 @@ if (__DEV__) {
1947319508

1947419509
return false;
1947519510
} else {
19511+
emitPendingHydrationWarnings(); // We might have reentered this boundary to hydrate it. If so, we need to reset the hydration
19512+
1947619513
if ((workInProgress.flags & DidCapture) === NoFlags$1) {
1947719514
// This boundary did not suspend so it's now hydrated and unsuspended.
1947819515
workInProgress.memoizedState = null;
@@ -19585,8 +19622,9 @@ if (__DEV__) {
1958519622
var wasHydrated = popHydrationState();
1958619623

1958719624
if (wasHydrated) {
19588-
// If we hydrated, then we'll need to schedule an update for
19625+
emitPendingHydrationWarnings(); // If we hydrated, then we'll need to schedule an update for
1958919626
// the commit side-effects on the root.
19627+
1959019628
markUpdate(workInProgress);
1959119629
} else {
1959219630
if (current !== null) {

0 commit comments

Comments
 (0)