Skip to content

Commit

Permalink
Move Hydration Mismatch Errors to Throw or Log Once (Kind of) (#28502)
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
sebmarkbage committed Mar 27, 2024
1 parent 0602001 commit 5add535
Show file tree
Hide file tree
Showing 16 changed files with 930 additions and 1,308 deletions.
2 changes: 1 addition & 1 deletion compiled/facebook-www/REVISION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4b8dfd6215bf855402ae1a94cb0ae4f467afca9a
f7aa5e0aa3e2aa51279af4b6cb5413912cacd7f5
42 changes: 40 additions & 2 deletions compiled/facebook-www/ReactART-dev.classic.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ if (__DEV__) {
return self;
}

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

var LegacyRoot = 0;
var ConcurrentRoot = 1;
Expand Down Expand Up @@ -3719,8 +3719,14 @@ if (__DEV__) {
}
}

function describeDiff(rootNode) {
return "\n";
}

var isHydrating = false; // This flag allows for warning supression when we expect there to be mismatches

var hydrationDiffRootDEV = null; // Hydration errors that were thrown inside this boundary

var hydrationErrors = null;

function prepareToHydrateHostInstance(fiber, hostContext) {
Expand Down Expand Up @@ -3777,6 +3783,35 @@ if (__DEV__) {
hydrationErrors.push(error);
}
}
function emitPendingHydrationWarnings() {
{
// If we haven't yet thrown any hydration errors by the time we reach the end we've successfully
// hydrated, however, we might still have DEV-only mismatches that we log now.
var diffRoot = hydrationDiffRootDEV;

if (diffRoot !== null) {
hydrationDiffRootDEV = null;
var diff = describeDiff();

error(
"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:\n" +
"\n" +
"- A server/client branch `if (typeof window !== 'undefined')`.\n" +
"- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
"- Date formatting in a user's locale which doesn't match the server.\n" +
"- External changing data without sending a snapshot of it along with the HTML.\n" +
"- Invalid HTML tag nesting.\n" +
"\n" +
"It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n" +
"\n" +
"%s%s",
"https://react.dev/link/hydration-mismatch",
diff
);
}
}
}

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

return false;
} else {
emitPendingHydrationWarnings(); // We might have reentered this boundary to hydrate it. If so, we need to reset the hydration

if ((workInProgress.flags & DidCapture) === NoFlags$1) {
// This boundary did not suspend so it's now hydrated and unsuspended.
workInProgress.memoizedState = null;
Expand Down Expand Up @@ -19904,8 +19941,9 @@ if (__DEV__) {
var wasHydrated = popHydrationState();

if (wasHydrated) {
// If we hydrated, then we'll need to schedule an update for
emitPendingHydrationWarnings(); // If we hydrated, then we'll need to schedule an update for
// the commit side-effects on the root.

markUpdate(workInProgress);
} else {
if (current !== null) {
Expand Down
42 changes: 40 additions & 2 deletions compiled/facebook-www/ReactART-dev.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ if (__DEV__) {
return self;
}

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

var LegacyRoot = 0;
var ConcurrentRoot = 1;
Expand Down Expand Up @@ -3484,8 +3484,14 @@ if (__DEV__) {
}
}

function describeDiff(rootNode) {
return "\n";
}

var isHydrating = false; // This flag allows for warning supression when we expect there to be mismatches

var hydrationDiffRootDEV = null; // Hydration errors that were thrown inside this boundary

var hydrationErrors = null;

function prepareToHydrateHostInstance(fiber, hostContext) {
Expand Down Expand Up @@ -3542,6 +3548,35 @@ if (__DEV__) {
hydrationErrors.push(error);
}
}
function emitPendingHydrationWarnings() {
{
// If we haven't yet thrown any hydration errors by the time we reach the end we've successfully
// hydrated, however, we might still have DEV-only mismatches that we log now.
var diffRoot = hydrationDiffRootDEV;

if (diffRoot !== null) {
hydrationDiffRootDEV = null;
var diff = describeDiff();

error(
"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:\n" +
"\n" +
"- A server/client branch `if (typeof window !== 'undefined')`.\n" +
"- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" +
"- Date formatting in a user's locale which doesn't match the server.\n" +
"- External changing data without sending a snapshot of it along with the HTML.\n" +
"- Invalid HTML tag nesting.\n" +
"\n" +
"It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n" +
"\n" +
"%s%s",
"https://react.dev/link/hydration-mismatch",
diff
);
}
}
}

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

return false;
} else {
emitPendingHydrationWarnings(); // We might have reentered this boundary to hydrate it. If so, we need to reset the hydration

if ((workInProgress.flags & DidCapture) === NoFlags$1) {
// This boundary did not suspend so it's now hydrated and unsuspended.
workInProgress.memoizedState = null;
Expand Down Expand Up @@ -19585,8 +19622,9 @@ if (__DEV__) {
var wasHydrated = popHydrationState();

if (wasHydrated) {
// If we hydrated, then we'll need to schedule an update for
emitPendingHydrationWarnings(); // If we hydrated, then we'll need to schedule an update for
// the commit side-effects on the root.

markUpdate(workInProgress);
} else {
if (current !== null) {
Expand Down
Loading

0 comments on commit 5add535

Please sign in to comment.