facebook / react

The library for web and native user interfaces.
https://react.dev
MIT License
227.51k stars 46.42k 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.

gaearon commented 2 years ago

Might be related to (or same as) https://github.com/facebook/react/issues/22833, but let's keep both open for now

marcusthelin commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

Yes, that issue is https://github.com/facebook/react/issues/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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

@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 commented 2 years ago

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 commented 2 years 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 🥳

dbashford commented 2 years ago

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 commented 2 years ago

@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 commented 2 years ago

@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 commented 2 years ago

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 commented 2 years ago

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 git@github.com: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` 6. Go through the prompts, give it a name and `cd` into the newly created project 7. `yarn add react react-dom` 8. `yarn add -D @types/react @types/react-dom` 9. Open up `app/entry.client.tsx` and migrate from `react-dom`'s `hydrate` to `react-dom/client`'s `hydrateRoot`
BleedingDev commented 2 years ago

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 commented 2 years ago

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

dbashford commented 1 year ago

Also causing issue with LastPass, can confirm Loom as well, causing issues across multiple plugins for us.

SokratisVidros commented 1 year ago

Apart from extensions, this issue is affecting libraries that hot load scripts or styles on bootstrapping. In Clerk we use this technique to ensure that users of Clerk-powered applications always have the latest fixes in their authentication components.

A workaround is to delay the hot loading of the components code on the client side. Unfortunately, this is not acceptable for authentication as it will increase the necessary load time before the user is able to interact with the page.

dejanr commented 1 year ago

Still an issue for react apps that render full document. If anyone is using browser extension that injects scripts during the page load, there is an hydration error occuring. Is it safe to use hydrate instead of hydrateRoot, since someone mentioned that it is working with react-dom hydrate as intended.

kamranayub commented 1 year ago

Happening with Grammarly and Vimeo Recorder too 😞 If I disable LastPass, and the other two, it's fine.

Ryongyon commented 1 year ago

This problem makes me unable to use Remix framework normally. Although we know that disabling Chrome extensions can solve the problem, we cannot ask our users to disable Chrome extensions. However, many Chrome extensions will inject content into Html documents now, so I wonder when the authorities will pay attention to this problem?

denchiklut commented 1 year ago

@gaearon It looks like the problem appears only when u try to stream html into document because of additional scripts from browser extensions. So when I tried to stream the app in the div#root it really worked fine and solved all hydration issues (because now hydration only work on our div#root which doesn't have any extra browser scripts etc..).

This is the working code example (branch: feat/stream)

OnurGvnc commented 1 year ago

I was able to resolve my issue by invoking clearBrowserExtensionInjectionsBeforeHydration() prior to executing hydrateRoot(). https://gist.github.com/OnurGvnc/31f03f0d5237b78224aa083493fda645

0Lucifer0 commented 1 year ago

I was able to resolve my issue by invoking clearBrowserExtensionInjectionsBeforeHydration() prior to executing hydrateRoot(). https://gist.github.com/OnurGvnc/31f03f0d5237b78224aa083493fda645

Yeah it's not really a good fix as it just remove all the potential dom changes... Still useful as a temp fix until better solution... not sure why this bug is still tagged as unconfirmed after 9 month when it's literally making react18 not production ready

Mihai-github commented 1 year ago

Hi, still having issues with this ... I saw/read a lot of issues and did not see a universal solution (if there is one) ... maybe someone can help me with an answer? Also on a side note... not sure it is related but I have CSS flickers and not sure if is related to this or not.

Thanks.

erwan-joly commented 1 year ago

Hi, still having issues with this ... I saw/read a lot of issues and did not see a universal solution (if there is one) ... maybe someone can help me with an answer? Also on a side note... not sure it is related but I have CSS flickers and not sure if is related to this or not.

Thanks.

Same for the css flickers not sure why. I don’t think it’s related as I didn’t use to have it it appeared recently

Mihai-github commented 1 year ago

@erwan-joly It's related to the hydration because I switched for testing this to react v17 and the flickers disappeared.

Ryongyon commented 1 year ago

Expect to be more of a Chrome problem than a React problem

In my research and observation, I have found that Chrome extensions that inject content into a page occasionally trigger this error, such as "React Developer Tools," which occasionally causes hydration errors. Later I read that Chrome extensions do not always inject content into a page at a fixed time, which means sometimes before the page is loaded, sometimes after the page is loaded. If you inject after the page is loaded, you won't get an error. If you inject before the page is loaded, you will naturally fail to hydrate.

Once people bring up the subject, Remix officials will just say it's a React problem and do nothing about it. Why is Remix the only SSR framework with this problem? Or does Remix just not care about the developer's feelings? Remember that most browser extensions now inject scripted content into the page, and if this isn't fixed, it means Remix won't work in production.

While we know that disabling Chrome extensions can fix this, we can't ask our users to disable Chrome extensions

I believe the reason the Remix website doesn't have this issue is because they are using the React 17 version, if they are using the latest version they will have this issue

lildesert commented 1 year ago

I experience the same issue with my Remix app and have to use hydrate instead of hydrateRoot to make it work properly.

In the meantime, other apps seem to be using React 18 with Remix without this issue. For example the react-router-website.

I didn't take the time to check the differences but maybe we could identify a workaround here?

martdavidson commented 1 year ago

Why is Remix the only SSR framework with this problem?

It's not, I've just spent an afternoon debugging why our production build was doing this with next.js - because I have lastpass installed, and it was injecting it's little icon into a username field.

Aprillion commented 1 year ago

Expect to be more of a Chrome problem than a React problem

In my research and observation, I have found that Chrome extensions that inject content into a page occasionally trigger this error...

@Ryongyon unfortunately, there is nothing that Chrome/Firefox extension makers can do about it, and all of those extensions coexisted with React for many years, so it's React 18 that made this problem materialize and thus it's up to React to fix it, we cannot expect thousands of extension makers to figure out how to deal with sudden influx of rogue React websites

e.g. https://github.com/darkreader/darkreader/issues/8842#issuecomment-1120342766

Aprillion commented 1 year ago

@gnoff I see you are assigned to this issue since June 2022, but it still has Status: Unconfirmed even after we provided reproduction steps and many people confirmed it's a problem with many SSR frameworks and many browser extensions ... How can the community help more, please?

Xiphe commented 1 year ago

Also running into this issue. In my case, the worst effect is due to react rebuilding the document, the injected <style />-tag from styled-components is being removed and the lib does not recover from this state (ref https://github.com/styled-components/styled-components/issues/3924)

Is there a way to tell react not to hydrate a nested part of the app at all? (suppressHydrationWarning will still overwrite the original contents) like <div><span>React manages this</span><span endOfReact={true}>React won't care about this</span></div>?

cayblood commented 1 year ago

I experience the same issue with my Remix app and have to use hydrate instead of hydrateRoot to make it work properly.

Can someone explain the tradeoff of using this workaround?

0Lucifer0 commented 1 year ago

I experience the same issue with my Remix app and have to use hydrate instead of hydrateRoot to make it work properly.

Can someone explain the tradeoff of using this workaround?

hydrate is deprecated as replaced by hydrateRoot. My understanding is that the main difference is around streaming SSR. I believe streaming SSR won't be possible with hydrate. It's pretty much like if you were running react 17.

cayblood commented 1 year ago

Remix folks, look no further than this repo for a React 18-compatible workaround: https://github.com/kiliman/remix-hydration-fix

gaearon commented 1 year ago

I see you are assigned to this issue since June 2022, but it still has Status: Unconfirmed

Just wanted to jump in here real quick — @gnoff has been focusing on different related things in this area for a while (several months). We just don't use issue labels a lot, so we don't always update them. From what I understand, we already fixed some issues in main (available as @next canary prereleases), and for others there is a bigger ongoing project. But I don't have exact details yet so I'll ask around.

gaearon commented 1 year ago

Regarding the fixes that already landed in main, it would help if you could confirm which of these issues still occur with react@next and react-dom@next prereleases.

adbutterfield commented 1 year ago

Problem remains with react@next and react-dom@next on my repo: https://github.com/adbutterfield/fast-refresh-express/tree/react-18

hrgui commented 1 year ago

I believe the problem still remains. https://github.com/hrgui/react-next-hydrateRoot-problem. I've updated the package deps to use react@next and react-dom@next

When Dark Reader runs on my example on http://localhost:4000, the server renders, then React checks if the client matches. Since Dark Reader has changes, I still see it re-render the entire document with the following error messages:

Uncaught Error: Hydration failed because the initial UI does not match what was rendered on the server.
An error occurred during hydration. The server HTML was replaced with client content in <#document>.
gnoff commented 1 year ago

Hey folks, here's a brief update on what I've been working on and what you can expect to be addressed in the next stable release of React.

This issue has attracted a wide array of failure modes related to hydration. Originally it was centered around trying to SSR the entire document and then call hydrateRoot(document,...) on the client. Since then the issues have expanded to include problems with any browser extension modifying the DOM before React has a chance to hydrate it. What I'd like to do is explain the current constraints of hydration, what should be fixed in the next stable, and what will continue to be problematic interop with extensions that we may or may not try to solve.

How hydration works today

When React hydrates your application it looks in the DOM for the next element that should have a partner host element in your app. If it sees a <div> it will expect the next html your apps renders to be a <div> as well. As you can imagine if an extension injects a <script> tag in the middle of your SSR'd HTML then this hydration algorithm is going to stumble upon it and expect you to render that <script> and when you don't (because it was injected by a browser extension) hydration will fail and some of your React app will fall back to client rendering where the DOM will be discarded and a new one will be constructed from the render).

React has always found hydrating <html>, <head>, and <body> tags challenging because these are the most commonly targeted DOM nodes for manipulation by 3rd party scripts and extensions and that is why for a long while React did not recommend rendering from the Document but instead use some <div id="my-root" /> embedded in the body. One affordance made though was that for these tags specifically React would ignore any extra Nodes it found when hydrating so even if a 3rd party script or extension did inject some extra DOM Nodes, hydration would continue without trying to hydrate them.

Another interop challenge is that when React falls back to client rendering, and it is hydrating the Document then it will remove the <html> Node and all children while it prepares the client rendered nodes to re-insert. This will remove 3rd party styles from the DOM and while most 3rd party stuff is resilient to this (it's a hostile environment, trying to patch someone elses HTML) they aren't all and this can break extensions in ways we don't like.

This is the state of React and hydration today and I believe the cause of many but not all of the issues reported in this issue so far. To summarize, React expects the HTML you render to match what is in the DOM in the same order and for <head> and <body> it is a little more tolerant of extra nodes appearing AFTER your React Nodes which it ignores. It might also remove and recreate the entire DOM which may not play nice with 3rd party scripts and extensions.

What is New

In the next stable a number of new features are going to be included which should alleviate some of the pain points above.

  1. React will treat <html>, <head>, and <body> as special types of host elements and will never remove them from the DOM. Even if React has to render the whold document from scratch, it will only remove the contents of these elements, preserving style/stylesheets (scripts don't need to be preserved because once they are in the DOM they execute regardless of whether they are removed or not)

  2. React will start to treat certain tags as hoistable. Things like <title>, <meta>, and <link> will under most circumstances be hoisted to the <head> independent of their rendered position in the Document. Along with these new hoisting semantics are new hydration semantics. Position in the Document won't govern a hydration match here so if the <head> has extra nodes injected in random places there should be limited impact.

  3. React will start to treat certain kinds of browser resources like stylesheets (with an opt-in) and async scripts (with an opt-out) as hositable and dedupable. I'll share more about this in a proper post somewhere else but similar to hoistable tags if you opt into resource semantics hydration will not use the normal linear flow

In summary, the vast majority of tags that get injected into <head> and <body> should avoid the normal linear flow of hydration and make collisions with your app's hydratable elements much harder to encounter.

What is not changing yet

React still expects normal (non-hoisted) tags to appear in the order rendered on the server without any intervening HTML Nodes added by 3rd party scripts and browser extensions. We also have not done anything to address hydration mismatches due to extensions modifying nodes deep in your body. For instance, in one of the referenced cases, a password manager adds an icon to an input field which breaks hydration. This is probably more of a prevalent problem in React 18 than in prior versions because hydration falls back to client rendering rather than patching up the DOM as it hydrates. We will consider if there are options for improving this situation in the future but this is not addressed by my work.

gnoff commented 1 year ago

When Dark Reader runs on my example on http://localhost:4000/, the server renders, then React checks if the client matches. Since Dark Reader has changes, I still see it re-render the entire document with the following error messages:

@hrgui thanks for checking that out. I took a look at this extension and it does inject sync scripts and style tags at the beginning of the <head>. This is surprising in some regard (there really isn't a reason to insert a script anywhere in particular) though the <style> perhaps requires some kind of precedence shadowing. What I can say is that given the contraints I outlined above it is expected that this extension interferes with hydration as it currently is implemented. I will consider what we could do to be more tolerant here

gaearon commented 1 year ago

Regarding the fixes that already landed in main, it would help if you could confirm which of these issues still occur with react@next and react-dom@next prereleases.

As a related note — it would also be good to check for each case that the causes of the problem are genuinely new in 18, and what exactly the root cause is in each case. E.g. is it related to streaming (initial hydration no longer means all HTML has arrived), to non-blocking hydration (startTransition and Suspense let us do it in chunks instead of blocking for the entire page), to hydrating the whole document, or to React's hardened behavior on mismatches when something mismatches (it now re-renders up to the closest Suspense boundary on the client instead of leaving the document in an inconsistent state).

I know it's not very easy to determine but I think it's really important to categorize each case so that we know where the bulk of the issues is. In some cases it could be easy userland fixes — e.g. deferring some script from executing until a certain point. Or telling React not to touch content controlled by something else with suppressHydrationWarning or even dangerouslySetInnerHTML. But it could also lead to some change in React — e.g. relaxing some heuristic in React so that it's less strict in cases where inconsistencies don't risk security bugs. Like some of the work @gnoff already did.


To give you an example, here's an example with a hydration that started appearing in 18: https://github.com/dabit3/nextjs-appdir-embed-testing/. It renders Twitter embed code (which has an HTML snippet and a script tag that would replace it with Twitter's client-rendered embed). The issue started happening because Next.js (thanks to React 18) can hydrate without blocking the page. It's better! But as a result, the Twitter embed code ran "too early" and modified the HTML before React hydrated. This caused the error.

My first instinct was to delay running the third-party script (see https://github.com/dabit3/nextjs-appdir-embed-testing/pull/1/commits/1ca23b6cb5190f861c0541a42907718b2dd2843c, Next's <Script> component does that by default). But actually, that was not fundamentally the right solution. Again, the problem is that the script replaced a piece of DOM owned by React. Even if this happened after hydration, React would still get confused if it tried to later update this part of the page during a state update. It's just that now this shows up as a hydration error (recovering with a clean client re-render) rather than a later error.

The actual correct solution in this case was to tell React that this part of the page is not owned by React — and so it shouldn't worry about it (see https://github.com/dabit3/nextjs-appdir-embed-testing/pull/1/commits/ab48f48de16294b1aa48777fc1f8b2bdcc6c2bab). This would prevent future client-side errors too because React would skip over that HTML.


This is to illustrate that grouping all these errors as "React 18 errors" makes it easy to miss the root of the problem. For each root problem, we'd like to either provide a fix, a workaround, or at least an explanation for why exactly this breaks, so that you can make an informed choice about some tradeoff. (E.g. if it's streaming in particular that broke something, you could disable that, and so on.) I can't promise we'll fix all of these cases — we likely won't — but the more information we have about the actual root causes, the better recommendations we will be able to give. And hopefully fix a few things in React too.

gaearon commented 1 year ago

We also have not done anything to address hydration mismatches due to extensions modifying nodes deep in your body. For instance, in one of the referenced cases, a password manager adds an icon to an input field which breaks hydration. This is probably more of a prevalent problem in React 18 than in prior versions because hydration falls back to client rendering rather than patching up the DOM as it hydrates.

Btw, I want to emphasize the other side of this as well. React 18 is more conservative about hydration errors in the sense that it re-renders the closest Suspense boundary with a clean client render, ignoring server HTML. While this is not very efficient, it is safer than what React did before (trying to "guess" which nodes to delete and update as it patches up the tree).

As a concrete example, suppose you're rendering a list of chat threads in a messenger app. They have avatars and names. Suppose you have a bug, and the user list on the server is in a slightly different order than when hydrated on the client.

React 17 would patch up each user's name to match the order from the client. However, it wouldn't patch up their attributes (it's too slow to iterate over each every time) — so their avatars would not match their names. Their profile links would also not match. If you're not careful, this could lead you to messaging a wrong user by mistake. And in any case, as a user, you wouldn't know which information is right. Do you trust the name, the avatar, or the link?

React 18 would not attempt to do this patch-up. It would blow away the server HTML up to the closest <Suspense> boundary (which would probably be somewhere around the list of users) and render that list on the client from scratch. That's a security/consistency feature. Now all links and avatars and names match up. Of course, it's not perfect — this is slower (in the bad case). It also doesn't catch all possible inconsistencies (like if only attributes were different). But I wanted to explain why this is a feature, not a bug. With granular <Suspense> boundaries and a way to opt-out for specific cases like timestamps via suppressHydrationWarning, this behavior offers a pretty good balance.

gaearon commented 1 year ago

I also wanted to add that we don't recommend to fix this by hydrating independent divs. At least not in long term. Hydrating the full document is the right strategy, and the work @gnoff described in https://github.com/facebook/react/issues/24430#issuecomment-1440427646 should help with that. But again, we need to look at each problem on a case-by-case basis. Different problems need different solutions.

scerelli commented 1 year ago

I also have the same issue using Next 13 and React 18.2.0.

This is my scenario:

The error doesn't happen in Incognito or if i disable Dashlane extension. It also happen with other extensions.