Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug: Hydration mismatch error due to plugins generating script tag on top #24430

Open
yongdamsh opened this issue Apr 24, 2022 · 76 comments
Open

Comments

@yongdamsh
Copy link
Contributor

React version: 18.0.0, 18.1.0-next-fc47cb1b6-20220404 (latest version in codesandbox)

Steps To Reproduce

  1. Install a plugin that creates a script tag at the top(ex: Apollo Client Devtools)
  2. Go to the demo in the new SSR suspense guide
  3. Open preview in a new window
  4. UI mismatch error occurs at hydration time

스크린샷 2022-04-24 오전 11 02 34

Link to code example: https://codesandbox.io/s/kind-sammet-j56ro?file=/src/App.js

The current behavior

If a script tag is inserted before the head tag due to the user's browser environment such as a plugin, it is judged as a hydration mismatch and the screen is broken.

2022-04-24.11.02.00.mov

The expected behavior

This problem may be a part that each third party needs to solve, but I'm wondering if it's possible to handle an exception in the hydration matching logic of React.

@yongdamsh yongdamsh added the Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug label Apr 24, 2022
@gaearon
Copy link
Collaborator

gaearon commented Apr 24, 2022

Might be related to (or same as) #22833, but let's keep both open for now

@marcusthelin
Copy link

marcusthelin commented Apr 24, 2022

Is the hydrateRoot function expecting the whole html document to be exactly what gets rendered by renderToPipeableStream? The best would be to just try to hydrate the React root element where the app is rendered?

It does seem that to use renderToPipeableStream I need to render the whole HTML document with a React component, but this is not ideal when e.g. using Vite with SSR in development, since it needs to transform the html to inject custom scripts.

@hrgui
Copy link

hrgui commented Apr 25, 2022

From my understanding, if anything else other than document was passed in into hydrateRoot, it doesn't seem to crash when I have chrome extensions that modify the DOM installed (e.g. Dark Reader / Apollo DevTools).

Here is the code sandbox: https://codesandbox.io/s/react-18-root-div-hydrateroot-1f5d5q?file=/src/Html.js:193-941

In the above example, I changed the following:

Html.js

export default function Html({ assets, children, title }) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="shortcut icon" href="favicon.ico" />
        <link rel="stylesheet" href={assets["main.css"]} />
        <title>{title}</title>
      </head>
      <body>
        <noscript
          dangerouslySetInnerHTML={{
            __html: `<b>Enable JavaScript to run this app.</b>`
          }}
        />
+        <div id="root">{children}</div>
-       {children}
        <script
          dangerouslySetInnerHTML={{
            __html: `assetManifest = ${JSON.stringify(assets)};`
          }}
        />
      </body>
    </html>
  );
}

App.js

export default function App({ assets }) {
  return (
    <Html assets={assets} title="Hello">
+         <AppContent />
-         <Suspense fallback={<Spinner />}>
-            <ErrorBoundary FallbackComponent={Error}>
-                <Content />
-            </ErrorBoundary>
=         </Suspense>
    </Html>
  );
}

+ export function AppContent() {
+  return (
+    <Suspense fallback={<Spinner />}>
+      <ErrorBoundary FallbackComponent={Error}>
+        <Content />
+      </ErrorBoundary>
+    </Suspense>
+  );
+ }

index.js:

- hydrateRoot(document <AppContent />);
+ hydrateRoot(document.getElementById("root"), <AppContent />);

I don't know if this crashes with Cypress though. The app doesn't seem to crash under cypress.

Cypress was adding

function $RC(a,b) {...} 

to the document. I'd assume it would crash if I hydrated the document.

@gaearon
Copy link
Collaborator

gaearon commented Apr 25, 2022

I don’t think the “stricter” behavior here is intentional. I’ll be checking with the team but my current understanding is that this is a bug.

@hrgui
Copy link

hrgui commented Apr 30, 2022

Something I've noticed is that when React does encounter a hydration mismatch, it attempts to fallback to client side rendering.

Which does show up in the example codesandbox (the one where we are hydrating the document):

Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.
Uncaught Error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.

However, it results in an application crash because of the next error:

 Failed to execute 'appendChild' on 'Node': Only one element on document allowed.

which is from the call stack:
appendChildToContainer <- insertOrAppendPlacementNodeIntoContainer (3) <- commitPlacement <- commitMutationEffectsOnFiber <- commitMutationEffects_complete <- commitMutationEffects_begin <- commitMutationEffects <- commitRootImpl

Then later another error is thrown:

Uncaught DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.

which is from the call stack:
removeChildFromContainer <- unmountHostComponents <- commitDeletion <- commitMutationEffects_begin <- commitMutationEffects <- commitRootImpl <- commitRoot <- performSyncWorkOnRoot <- flushSyncCallbacks <- commitRootImpl


What I am wondering about is: In the case of falling back to client side rendering, why does React do appendChild instead of replaceChild? If it was replaceChild, there wouldn't be an app crash in this case, but at the cost of needing to fall back to client side rendering.

@gaearon
Copy link
Collaborator

gaearon commented Apr 30, 2022

Yes, that issue is #22833. I believe the fix we wanted to do is changing which top-level element we modify. (Maybe body instead of html?) It would be nice to not have to introduce a separate “host config” method (which we’d have to do if we added a call to “replace”). So ideally the fix should use the methods we already use.

@rdadoune
Copy link

Not an ideal solution but in my use case, I'm only concerned with generating/modifying the head during SSR, and the following hack allows me to work around errors that occur as the result of modifications to the head outside of React by Cypress, various chrome plugins, etc.

const Head: React.FC = () => {

  if (globalThis.document?.head) {
    return (
      <head dangerouslySetInnerHTML={{ __html: document.head.innerHTML }} />
    );
  }

  return (
    <head>
      {/* ... Do important things on the server */}
    </head>
  );
};

This is especially useful for me because even with current fixes added to react-dom@next that allow the client to "recover", doing so wipes out all styles generated by @emotion that have been collected into the head.

@adbutterfield
Copy link

adbutterfield commented Jun 3, 2022

I think I have the same error. But mine is from using styled-components. Initially, they put a style tag in the body, but then the style tag gets moved up to the head. You can check this repo here: https://github.com/adbutterfield/fast-refresh-express/tree/react-18

I tried just now using react@next/react-dom@next, but I still get the error.

Of course, it's always possible that I'm doing something stupid in my code...

@Mordred
Copy link

Mordred commented Jun 15, 2022

I tried React 18.2.0, but it still breaks the page when Apollo Client DevTools extension is used. So my ugly solution for this problem is fix the DOM before hydrateRoot:

document.querySelectorAll('html > script').forEach((s) => {
  s.parentNode.removeChild(s);
});

const root = hydrateRoot(
  document,
  <React.StrictMode>
    <AppWithFullDocument />
  </React.StrictMode>,
);

Note: You can replace the selector more strict query .. e.g. 'html > script[src^=chrome-extension]'

(remix-run/remix#2947)

@dbashford
Copy link

Getting the issue with Cypress tests, as suggested in Remix Discord, my workaround for those coming here:

if (process.env.NODE_ENV === 'test') {
  require('react-dom').hydrate(<RemixBrowser />, document);
} else {
  hydrateRoot(document, <RemixBrowser />);
}

🙏 for a fix soon, thx all!

@gaearon
Copy link
Collaborator

gaearon commented Jun 22, 2022

@dbashford Which in particular issue are you hitting? We’ve released a short-term fix for the most urgent part of the problem in 18.2 (document fallback didn’t work at all). Now it should work by doing a full client render but this will lose the third party things. There is a possible fix to do something smarter but it’s a bigger project and a bit further on the timeline. So it would help to know what exactly broke for you. Is there a repro? Particular scenario? Thanks.

@dbashford
Copy link

Mine is the Cypress case. Everything works fine in dev and in prod, but Cypress when it kicks up fails (418), and only after making the hydrateRoot switch. I haven't dug into it too far to really understand the problem, but I've read in this thread and in the Remix discord that the problem may be that Cypress monkey's with the head causing the hydration issue. I admit that SSR/hydration are still voodoo/witchcraft to me, so I'm struggling a bit to work my way through it. Baby steps.

@CanRau
Copy link

CanRau commented Jun 22, 2022

Hey @Mordred thanks a lot for this quick hack, I modified it slightly to also get rid of inputs within html as the Yoroi extensions injects a hidden input directly into the <html/> (not <body/>)

document.querySelectorAll("html > script, html > input").forEach((s) => {
  s.parentNode?.removeChild(s);
});

Using this now in my Remix Deno Stack with Streaming 🥳

@dbashford
Copy link

dbashford commented Jun 22, 2022

Nevermind this, was local NODE_ENV issue (cypress issue above is still a problem, though)

Also having issues with google analytics writing script tags to the head

image

In this case /blog/gtag pulls in the tag manager snippet...

Google tag manager starts adding scripts to the head, like google-analytics, and things go sideways from there. I've got microsite where this seems to be working, and some where it isn't.

@gaearon
Copy link
Collaborator

gaearon commented Jun 22, 2022

@dbashford Can you share a minimal project? It always helps to have concrete examples to check fixes against. This goes for everyone else too.

@dbashford
Copy link

dbashford commented Jun 22, 2022

@gaearon Can call off the dogs on that latest comment, was something I introduced myself while trying to debug a real hydration issue with dates.

The Cypress issue is still a problem, that's all local and consistent. I'll work to get a repro up over the next few weeks when I get a second.

@adbutterfield
Copy link

I just updated my repo with some instructions to more easily reproduce the issue, and test that it works when not using styled components.

See here: https://github.com/adbutterfield/fast-refresh-express/tree/react-18

@camjackson
Copy link

Hi @gaearon I have a repro repo for the hydration errors with cypress. It's not exactly "minimal", it's a freshly-created Remix grunge stack app, which has quite a bit of stuff in it. Here it is: https://github.com/camjackson/remix-cypress-error.

Here are the steps to repro the error locally:

  1. git clone [email protected]:camjackson/remix-cypress-error.git
  2. cd remix-cypress-error
  3. yarn
  4. yarn test:e2e:run

The test will fail with a hydration error. To show that it's related to react 18, you can open up app/entry.client.tsx and comment/un-comment a few lines to switch it from hydrateRoot back to the old hydrate. Then run the e2es again and it will pass.

To debug it further, instead of yarn test:e2e:run you can do yarn test:e2e:dev to fire up cypress in interactive mode and see the test fail in a real browser.

Oh and one final note, if you just start the app normally with yarn dev you should see that there are no errors. It only happens with cypress, presumably because cypress injects something extra into the document.

For completeness, here's how I created this repo from scratch:
  1. yarn create remix --template remix-run/grunge-stack
  2. Go through the prompts, give it a name and cd into the newly created project
  3. yarn add react react-dom
  4. yarn add -D @types/react @types/react-dom
  5. Open up app/entry.client.tsx and migrate from react-dom's hydrate to react-dom/client's hydrateRoot

@BleedingDev
Copy link

I have replicated same behaviour - it can be replicated anywhere with hydrateRoot with any Chrome extension that injects the script. It then switches to client-side rendering.

Uncaught Error: Hydration failed because the initial UI does not match what was rendered on the server.

@0Lucifer0
Copy link

0Lucifer0 commented Jun 27, 2022

it's also preventing loom (extension) to work as the extension add a to the dom. Seems like it just break any extension that update the dom and having a workaround every potential extension is going to be a nightmare

fix for loom

document.querySelectorAll('loom-container').forEach((s) => {
    s.parentNode.removeChild(s);
});

additionally this also cause warning if the extension is adding attributes like this one with colorzilla
image

@Danones
Copy link

Danones commented Jan 8, 2024

Hey everyone 👋🏽

I am currently facing the same issue on my project. I have created the following reproduction repo , in my case I use Remix + Vite + Tailwind.

But the hydration issues have been around long before the Vite integration. It was only with the introduction of Vite, that this was most noticeable because, on Save, and after the HMR kicks in you will see the styles being removed from the DOM.

The following solves my issue:

  • updating react and react-dom following versions:
    "react": "^18.3.0-next-fccf3a9fb-20230213",
    "react-dom": "^18.3.0-next-fccf3a9fb-20230213",

And this fixes my styling issues despite continuing to get hydration erros in the console.

  • Disabling all extensions, or even opening a page in incognito mode ( the important stuff here is having all extensions disabled). This also solves the issue but I do not think this is a feasible option.

Knowing now what I know, the root cause might be react related but I still left an issue at vite's repo and I am wondering if remix staff can do something about this in the meantime.

@giankotarola
Copy link

my hydration error got fixed with:

"react": "^18.3.0-canary-3d9b20132-20240124",
"react-dom": "^18.3.0-canary-3d9b20132-20240124",

For testing i added back the cloudflare email obfuscated script injection + enable back the chrome extensions that triggers the hydration issue for me, Apollo client dev tools / Requestly and have not seen the issue 🙌

@rcfrias
Copy link

rcfrias commented Mar 1, 2024

I cant believe this is still a problem for the past 2 years. I have LastPass installed, and even a Hello World app with ONE input tag type "text" and a placeholder "email" is causing the hydration issue. and no, suppressHydrationWarning does not fix the problem. (placed it everywhere without luck) :(

@HakanSungur
Copy link

I'm getting the same error and I'm looking for a solution.

@Omar-Abdul-Azeez
Copy link

injected elements with style attribute throw an ssr mismatch error... why is this not fixed yet...

@grundmanise
Copy link

With all due respect @Omar-Abdul-Azeez, but it's an OS project..

@johnlomat
Copy link

Can confirm LastPass browser extension causing the hydration mismatch.

@einar-hjortdal
Copy link

Can confirm LastPass browser extension causing the hydration mismatch.

Can you please tell me if this happens on 18.3.1 and on canary?

@yemi
Copy link

yemi commented Aug 9, 2024

Seems react 19 (beta) might fix this, for me at least. Renders nicely with browser addons without errors

@wilbertcaba
Copy link

Can confirm LastPass browser extension causing the hydration mismatch.

Can confirm the same. Lastpass browser extension was causing error.

@digitaldjango
Copy link

Lastpass issue confirmed here as well. Took me a while to figure it out and ended up here..

@JamesLeBoeuf
Copy link

Seems react 19 (beta) might fix this, for me at least. Renders nicely with browser addons without errors

Tried this, unfortunately didn't work for me. Confirmed still an issue with LastPass browser extension causing it.

@allicanseenow
Copy link

Still seems to be an issue when testing with Remix and React 18.3.1. It happens when the browser tab has been inactive for some time and it is focused again.

@bialasky
Copy link

bialasky commented Oct 2, 2024

same happening with extension "I still don't care about cookies".

@phil294
Copy link

phil294 commented Oct 29, 2024

Can confirm that all our issues introduced by upgrading to React 18 were solved by upgrading to React 19 🎉️ (currently "react": "^19.0.0-rc-0bc30748-20241028"). Both stuff injected by extensions into <html> or <head> and altered dom deep nested in the app.

@triemstr
Copy link

triemstr commented Oct 31, 2024

I still get the warning with LastPass on the Oct 29 2024 React 19 and React-Dom 19 versions 19.0.0-rc-603e6108-20241029...but I love the new messaging. It shows git-style "minus" signs to show the difference between the server and client renderings, not to mention way better explanations and formatting. I think it is worth showing a screenshot for those who don't have this level of detail in React 18. Kudos to developers to analyze the issue to this level of detail.

image

UPDATE: I submitted this issue in the LastPass community. Not that it will go anywhere...perhaps React can still do something about working with plugins (?)

@raphaelbadia
Copy link

raphaelbadia commented Nov 4, 2024

if lastpass adds the tag suppressHydrationWarning I believe that the error could go away.

At least that's how jam chrome extension works

Screenshot 2024-11-04 at 13 42 18

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests