From f6aa145358688b188e0b92c227a89c0ae80a035c Mon Sep 17 00:00:00 2001 From: sebmarkbage Date: Wed, 27 Mar 2024 00:06:26 +0000 Subject: [PATCH] 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 https://github.com/facebook/react/commit/f7aa5e0aa3e2aa51279af4b6cb5413912cacd7f5. --- .../cjs/ReactTestRenderer-dev.js | 44 +++++++++++++++++-- .../Libraries/Renderer/REVISION | 2 +- .../implementations/ReactFabric-dev.fb.js | 44 +++++++++++++++++-- .../ReactNativeRenderer-dev.fb.js | 44 +++++++++++++++++-- 4 files changed, 124 insertions(+), 10 deletions(-) diff --git a/compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react-test-renderer/cjs/ReactTestRenderer-dev.js b/compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react-test-renderer/cjs/ReactTestRenderer-dev.js index 75e90c6806d1a..764ab1715bafa 100644 --- a/compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react-test-renderer/cjs/ReactTestRenderer-dev.js +++ b/compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react-test-renderer/cjs/ReactTestRenderer-dev.js @@ -7,7 +7,7 @@ * @noflow * @nolint * @preventMunge - * @generated SignedSource<<76f5e861be11fcf494a0e765f9e7dd59>> + * @generated SignedSource<<8afa90ff88a5d63d19c6d736d18ddb41>> */ "use strict"; @@ -2669,8 +2669,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) { @@ -2727,6 +2733,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 @@ -16859,6 +16894,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; @@ -16962,8 +16999,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) { @@ -25482,7 +25520,7 @@ if (__DEV__) { return root; } - var ReactVersion = "19.0.0-canary-57dfa024"; + var ReactVersion = "19.0.0-canary-f7ba3caa"; // Might add PROFILE later. diff --git a/compiled-rn/facebook-fbsource/xplat/js/react-native-github/Libraries/Renderer/REVISION b/compiled-rn/facebook-fbsource/xplat/js/react-native-github/Libraries/Renderer/REVISION index d0631851c63f1..fd582252d49aa 100644 --- a/compiled-rn/facebook-fbsource/xplat/js/react-native-github/Libraries/Renderer/REVISION +++ b/compiled-rn/facebook-fbsource/xplat/js/react-native-github/Libraries/Renderer/REVISION @@ -1 +1 @@ -4b8dfd6215bf855402ae1a94cb0ae4f467afca9a +f7aa5e0aa3e2aa51279af4b6cb5413912cacd7f5 diff --git a/compiled-rn/facebook-fbsource/xplat/js/react-native-github/Libraries/Renderer/implementations/ReactFabric-dev.fb.js b/compiled-rn/facebook-fbsource/xplat/js/react-native-github/Libraries/Renderer/implementations/ReactFabric-dev.fb.js index 28dc69ed7968a..8abff515072dd 100644 --- a/compiled-rn/facebook-fbsource/xplat/js/react-native-github/Libraries/Renderer/implementations/ReactFabric-dev.fb.js +++ b/compiled-rn/facebook-fbsource/xplat/js/react-native-github/Libraries/Renderer/implementations/ReactFabric-dev.fb.js @@ -7,7 +7,7 @@ * @noflow * @nolint * @preventMunge - * @generated SignedSource<> + * @generated SignedSource<<7fb7b6a99b100686fb7a03fec927ee74>> */ "use strict"; @@ -6149,8 +6149,14 @@ to return true:wantsResponderID| | } } + 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) { @@ -6207,6 +6213,35 @@ to return true:wantsResponderID| | 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 @@ -21216,6 +21251,8 @@ to return true:wantsResponderID| | 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; @@ -21319,8 +21356,9 @@ to return true:wantsResponderID| | 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) { @@ -29829,7 +29867,7 @@ to return true:wantsResponderID| | return root; } - var ReactVersion = "19.0.0-canary-1559f120"; + var ReactVersion = "19.0.0-canary-88667624"; function createPortal$1( children, diff --git a/compiled-rn/facebook-fbsource/xplat/js/react-native-github/Libraries/Renderer/implementations/ReactNativeRenderer-dev.fb.js b/compiled-rn/facebook-fbsource/xplat/js/react-native-github/Libraries/Renderer/implementations/ReactNativeRenderer-dev.fb.js index ef795247c16b9..af16a96248cce 100644 --- a/compiled-rn/facebook-fbsource/xplat/js/react-native-github/Libraries/Renderer/implementations/ReactNativeRenderer-dev.fb.js +++ b/compiled-rn/facebook-fbsource/xplat/js/react-native-github/Libraries/Renderer/implementations/ReactNativeRenderer-dev.fb.js @@ -7,7 +7,7 @@ * @noflow * @nolint * @preventMunge - * @generated SignedSource<> + * @generated SignedSource<<86b2303b8da7a666cdb52b8c48916c65>> */ "use strict"; @@ -6444,8 +6444,14 @@ to return true:wantsResponderID| | } } + 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) { @@ -6502,6 +6508,35 @@ to return true:wantsResponderID| | 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 @@ -21254,6 +21289,8 @@ to return true:wantsResponderID| | 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; @@ -21357,8 +21394,9 @@ to return true:wantsResponderID| | 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) { @@ -30269,7 +30307,7 @@ to return true:wantsResponderID| | return root; } - var ReactVersion = "19.0.0-canary-cce7aa34"; + var ReactVersion = "19.0.0-canary-9011959f"; function createPortal$1( children,