facebook / react

The library for web and native user interfaces.
https://react.dev
MIT License
229.06k stars 46.86k forks source link

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

Open yongdamsh opened 2 years ago

yongdamsh commented 2 years ago

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.

https://user-images.githubusercontent.com/4126644/164953071-14546c74-d9ab-4a6f-8f99-6712f29c6dd6.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.

gnoff commented 1 year ago

@scerelli I tried to see if this extenion's source code is available but wasn't able to find it on github. Do you know if it is reviewable?

Also, when the hydration fails is the entire app broken or does the app recover and there is just an error in the console?

Can you link a repro?

scerelli commented 1 year ago

@gnoff here you go: https://github.com/scerelli/hydration-issue-reproduction Screenshot 2023-03-08 at 21 04 37

Simply install the Dashlane extension and create a free account. However, this issue may also occur with any other browser extension that modifies the DOM.

I have observed that in my situation, the problem occurs when I am using Yup with React Hook Form. However, I am uncertain about the cause of the problem since the component that is displaying the issue is also a client component. Interestingly, on a Next 12/React 17 setup, the same code does not trigger the error.

mdodge-ecgrow commented 1 year ago

I'll be following this issue closely. I was attempting to create an app using remix.run and couldn't even get an initial basic build working because of this issue and I run a lot of browser extensions.

Just me thinking out loud here, but it's too bad the browsers can't enforce some sort of standard for extensions for how they get to manipulate the head and/or body content. Seems to me if there was that set standard, it would be a lot easier to React to look for and account for those changes.

gnoff commented 1 year ago

@scerelli thanks for the repro. I was also hopign to see the dashlane source but I can tell they are injecting dom nodes and adding attributes deep within the application.

React was not updated to be more tolerant of deep mismatches and instead focussed on handling cases in the head and body more effectively. For a lot of 3rd party stuff this is sufficient because that's all they do. But whenever an extension modifies the HTML deeply in the document React can't tell whether that was because you rendered different stuff on server/client or whether it was intentional.

The reason this was not a probem in React 17 is that version just does an insert and patches up the DOM rather than falling back to client rendering.

The fact that you get a bunch of big red errors is offputting and we should figure out a way to surface meaningful info for devs in dev mode to figure out if their app is not correctly hydrating in the baseline case but not cause undo worry when there are incidental hydration errors which are always going to happen especially for users that use 3rd parties that modify the DOM.

The good news is that your repro demonstrates that React can now effectively recover from a hydration mismatch even when the entire document is rendered by it. Before this would have wrecked the whole app because the html element would be unmounted in an ineffective way and it would fail to remount correctly.

I'll continue to gather info from these reports and may someday pursue a more ambitous look at coexisting with extensions deep in the DOM but I don't have anything planned for that at the moment. Thanks for taking the time to report this

scerelli commented 1 year ago

@gnoff Thanks to you for taking the time to explain further what's going on. Great work!

ErwinAI commented 1 year ago

Hello @gnoff @gaearon ,

I am a developer of a dev tool chrome extension (Tailscan) and thus far, three of my users have reported issues in combination with Remix / React 18. I've been following this issue for a while now and want to chip in, just to give an extra insight into the issue. I don't use React / Remix myself and so I can't easily replicate things, but if it's useful, I can give you access to my private repository containing the chrome extension's code and a packaged version, just in case you want or need to dive in deeper. In any case, I very much appreciate the extensive information both of you have provided, thank you!

All three users that have ran into the issue, receive the same errors as others have described above. It's either hydration errors or did not expect server HTML to contain ... errors.

One user reported the following error:

Warning: Did not expect server HTML to contain a <tailscan-container> in <html>.

Another user wrote this:

Changing the browser extension settings to only load the extension when clicking on the extension icon, instead of automatically on page load, fixes the problem on first page load. If I refresh the page a lot of times I can be lucky that the extension loads after the page is loaded (the code for the service worker pops up in the source tab in the dev tools a little delayed when this happens). My react version is set to "^18.2.0"

Furthermore, that same user reports:

I can consistently make the extension work by doing the following:

  1. Set site access to "On click" (referring to the aforementioned setting. In Chrome, right click the extension icon > "this can read and change site data" > "when you click the extension")
  2. Access the site in a newly open tab If I refresh the tab (not opening a new tab) it gets the hydration error where Tailscan won't load.
  3. If I open a new tab again Tailscan will work without any problems even though I navigate around, refresh the page etc. It is only after Tailscan has been initialized/loaded the first time, it breaks on subsequent refreshes.

The chrome extension does a couple of things within the DOM:

  1. There is a div element inserted containing a shadow DOM underneath the <body> element in the <html> element.
  2. A specific class is added to an element that is being inspected (sidenote: removed in next version)
  3. There are two attributes added to an element that is being inspected (data-tailscan-id and toggled-off-classes)
  4. There are <style> elements inserted at the bottom of the <head> element
  5. It adds and removes classes on an element that is being inspected (core functionality of this devtool)

The way this chrome extension works is very similar to other extensions such as password managers inserting things in <input> elements, other devtool extensions, etc.

I think the explanation of the users match up with the explanation @gaearon gave. Furthermore, the changes in the next stable that @gnoff mentions, will help solve some the issues I have. Two unknown for me are still:

  1. If custom (data) attributes added to elements in the element will be ignored or respected, or if that will still cause issues in the next stable.
  2. If changing classes on an element in the tag will still cause issues in the next stable.

And if either of them does cause problems, whether there is any way to go about it that doesn't break the app.

19Qingfeng commented 1 year ago

åI want to know the result of this problem.I have tried many ways, but I still can't solve the problem.

roggc commented 1 year ago

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 🥳

What worked for me was:

[...document.querySelectorAll("html>*")]
  .filter((elem) => !elem.matches("head,body"))
  .forEach((s) => {
    s.parentNode?.removeChild(s);
  });

This removes anything added to html from extensions like grammarly. Also add suppressHydrationWarning={true} in the html and body tags in the case of grammarly. I faced this situation in production in an app created with npx create-rsc-app@latest my-app --ssr (it's a setup with RSC and SSR I've made myself).

Mordred commented 1 year ago

Today I learned

Not every script tags and style tags comes from browser extensions. Kaspersky Internet Security -> Anti-Banner feature is injecting HTML tags on the network layer (with MiTM, but that is not relevant now :D). This effectively triggers hydration errors.

For now: I'm extending my querySelectorAll to

  document
    .querySelectorAll(
      'html > *:not(body, head), script[src*="extension://"], link[href*="extension://"], script[src*="scr.kaspersky-labs.com"], link[href*="scr.kaspersky-labs.com"]',
    )
    .forEach((s) => {
      s.parentNode.removeChild(s);
    });

If this will not work I swear I will add data-my-tag to every script, link, and style tag which were generated by my server 😂

There is also another problem. React is trying to recover but it replace all HTML elements with new ones. This breaks CSS-in-JS solutions e.g. Emotion, because they already have references to old elements where they are still injecting their styles.

I'm using the MUI library in one of my projects and after the hydration error, all styles are gone.

There is an onRecoverableError but it is called after the DOM was changed by React.

Can we have a callback that will be called before React tries to recover from the hydration mismatch? (@gnoff @gaearon)

I would like to make a snapshot of DOM for better debugging of what people have injected in their browsers. Also Emotion (or another) library can be reset (clear their elements refs) before react switch to client-side rendering (An error occurred during hydration. The server HTML was replaced with client content in <#document>.).

Danones commented 10 months ago

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:

    "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.

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 commented 9 months ago

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 commented 8 months ago

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 commented 5 months ago

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

Omar-Abdul-Azeez commented 3 months ago

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

grundmanise commented 3 months ago

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

johnlomat commented 3 months ago

Can confirm LastPass browser extension causing the hydration mismatch.

einar-hjortdal commented 3 months ago

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 commented 3 months ago

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

wilbertcaba commented 2 months ago

Can confirm LastPass browser extension causing the hydration mismatch.

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

digitaldjango commented 2 months ago

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

JamesLeBoeuf commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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

phil294 commented 1 week ago

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 commented 1 week ago

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 commented 4 days ago

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