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

Interaction between Remix, Lastpass, and <link rel=stylesheet> and (possibly) @import #4175

Closed
giltayar opened this issue Sep 11, 2022 · 24 comments

Comments

@giltayar
Copy link

giltayar commented Sep 11, 2022

What version of Remix are you using?

1.7.0

Steps to Reproduce

  1. Install Lastpass (I think you should also be registered...?)
  2. git clone https://github.com/giltayar/remix-import-css-lastpass-hydration-error-reproduction
  3. npm install
  4. npm run dev
  5. This should show the page with no errors in console

Now...

  1. Edit root.tsx and comment in the <link rel=stylesheet>.

Or...

  1. Edit root.tsx and comment in the <style> tag.

Expected Behavior

Opening the page in Chrome or Firefox: No errors in the console

Actual Behavior

Errors in console*:

For Firefox, DevTools must be open (with "disable cache" checked in the Network tab). For Chrome, even closed will trigger the problematic behavior (!)

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

and...

.
link
Scripts@http://localhost:3000/build/_shared/chunk-IMF64WG6.js:2851:7
body
html
App
RemixRoute@http://localhost:3000/build/_shared/chunk-IMF64WG6.js:2529:20
Routes2@http://localhost:3000/build/_shared/chunk-IMF64WG6.js:2513:7
Router@http://localhost:3000/build/_shared/chunk-IMF64WG6.js:741:7
RemixCatchBoundary@http://localhost:3000/build/_shared/chunk-IMF64WG6.js:1018:28
RemixErrorBoundary@http://localhost:3000/build/_shared/chunk-IMF64WG6.js:944:5
RemixEntry@http://localhost:3000/build/_shared/chunk-IMF64WG6.js:2406:20
RemixBrowser@http://localhost:3000/build/_shared/chunk-IMF64WG6.js:3150:27

Note that the error also happened to me when using a @import in an "emotion css" global rule (that is how I found it out). After much research, I found the problem also occurs with link rel. You have a reproduction of both in this repo.

Also note that Lastpass Definitely triggers the problem, but playing around with this reproduction shows me that all the other parameters are fluid in regards to reproducability:

  1. Whether Chrome or Firefox
  2. Whether devtools is open
  3. Whether "disable cache" is turned on
  4. Whether using links function, link rel in head, or using @import.
@tshddx
Copy link

tshddx commented Sep 12, 2022

I believe this is a very common issue with React 18 that mostly affects Remix apps because Remix puts React in charge of rendering the entire document. It is apparently also an issue in other frameworks as well. Apparently React 18.2 might fix some of these bugs. Have you tried 18.2?

@tshddx
Copy link

tshddx commented Sep 12, 2022

However, it appears that Remix users are reporting the issue persists in React 18.2:

#2570 (comment)

@giltayar
Copy link
Author

Yes, we're using the latest remix and React 18.2.0.

@giltayar
Copy link
Author

Some notes to anybody arriving here:

  1. As @tshddx said, this is probably the same bug as React 18 : Hydration failed because the initial UI does not match what was rendered on the server. #2570
  2. It is caused by Chrome/FF extensions that mutate the DOM (probably before React has a chance to hydrate)
  3. It happens mostly in Remix because Remix renders/hydrates the whole HTML, while others render/hydrate only a <div id=root />
  4. The problem is not just the error in the console. We use Emotion and the error causes it not (not sure why) to not add <style> tags to the header, and so the page hydrates without styles.
  5. There is a workaround: replace hydrateRoot with pre-React 18 hydrate (in Remix, this is entry.client.tsx)

@JAD3N
Copy link

JAD3N commented Oct 27, 2022

I was able to reproduce this by:

  1. Using Firefox (tried on Chrome but it worked?) install the LastPass addon.
  2. Log in to LastPass addon otherwise it works.
  3. Use the Chakra UI template (which uses Emotion): npx create-remix@latest --template examples/chakra-ui
  4. Add a font import: npm i @fontsource/inter
  5. Add links export to root layout:
import fontStyles from "@fontsource/inter/variable.css";

export const links: LinksFunction = () => [
	{
		rel: "stylesheet",
		href: fontStyles,
	},
];
  1. Navigate to app and hard refresh if necessary (toggling Firefox cache required a hard refresh to repeat the error for me).

It appears that the Emotion styles are being removed and never re-added for client-side rendering? The workaround by @giltayar works but as mentioned not being able to utilize the newer React features is a bit cumbersome.

Edit: After testing some more on Chrome it started happening as well (not sure why though).

Edit: I did some more research and found this workaround that allowed me to use the React 18 features. I found this by looking at: facebook/react#24430

function hydrate() {
  const emotionCache = createEmotionCache({ key: "css" });

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

    hydrateRoot(
      document,
      <StrictMode>
        <CacheProvider value={emotionCache}>
          <RemixBrowser />
        </CacheProvider>
      </StrictMode>
    );
  });
}

@KasparRosin
Copy link

KasparRosin commented Nov 1, 2022

react issue reference: facebook/react#24430

@tamm
Copy link

tamm commented Nov 3, 2022

Had some similar issues, I came up with this to silence the errors but do not recommend using this in production as it will kill a lot of browser plugin functionality.

function clearBrowserPluginInjectionsBeforeHydration() {
  if (document.body.dataset) {
    Object.keys(document.body.dataset).map((attribute) => {
      delete document.body.dataset[attribute];
    });
  }

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

function hydrate() {
  startTransition(() => {
    clearBrowserPluginInjectionsBeforeHydration();

    hydrateRoot(
      document,
      <StrictMode>
        <ClientCacheProvider>
          <RemixBrowser />
        </ClientCacheProvider>
      </StrictMode>
    );
  });
}

@nicolaserny
Copy link

Some notes to anybody arriving here:

  1. As @tshddx said, this is probably the same bug as React 18 : Hydration failed because the initial UI does not match what was rendered on the server. #2570
  2. It is caused by Chrome/FF extensions that mutate the DOM (probably before React has a chance to hydrate)
  3. It happens mostly in Remix because Remix renders/hydrates the whole HTML, while others render/hydrate only a <div id=root />
  4. The problem is not just the error in the console. We use Emotion and the error causes it not (not sure why) to not add <style> tags to the header, and so the page hydrates without styles.
  5. There is a workaround: replace hydrateRoot with pre-React 18 hydrate (in Remix, this is entry.client.tsx)

Sadly, the workaround with hydrate instead of hydrateRoot does not work anymore with Remix 1.7.X
When I upgrade from remix 1.6.8 to 1.7, it breaks my app by deleting my style component styles after hydration.
I know that css-in-js is not the recommended way. But when you have legacy code, it's a lot of code to migrate...

@KasparRosin
Copy link

KasparRosin commented Nov 10, 2022

@nicolaserny Weird, I'm using remix 1.7.2 with the hydrate fix and it works for me using styled-components.
There is an issue with styled-components, where sometimes the order of styles is messed up, but I believe it to be unrelated to the hydrate function.

@nicolaserny
Copy link

@KasparRosin, I don't think it's an order issue. First, I see the style tag in the head section. After the hydrate call, the style tag is completely removed (so I have no more CSS styles). I tested with remix 1.7.5 and react 18.2.0 with Chrome + extensions such as Loom. I will try to create a basic example.

@nicolaserny
Copy link

@KasparRosin It's pretty easy to reproduce. I downloaded the Remix styled component example: https://github.com/remix-run/examples/tree/main/styled-components.
First, I used remix 1.6.8. It works nicely even with a browser with extensions that modify the dom.
Next, update to remix 1.7.5. Now, the CSS styles are removed just after hydration when I use a browser with extensions. Interesting fact: the example does not use React 18 but React 17.0.2.

@MichaelDeBoey
Copy link
Member

This is probably because one of your browser extensions is injecting code : https://remix.run/pages/gotchas#browser-extensions-injecting-code

@dbashford
Copy link

This isn't just a warning, this is a breakage. My app doesn't function properly. I cannot leverage React 18 hydration. Styles entirely broken. Given the original reporter is using emotion, guessing his is too?

@dmarkow
Copy link
Contributor

dmarkow commented Nov 21, 2022

I can confirm emotion actually broke from this and it wasn't just a harmless/annoying warning (react-select uses it and the drop-downs become unusable). My only options were rolling back to React 17 rendering, or wrapping anything that uses emotion in a <ClientOnly>...</ClientOnly> block.

@dbashford
Copy link

dbashford commented Nov 21, 2022

In our case we use Chakra which leverages emotion for everything. So this is sadly neither just a gotcha nor something we can easily work around. I haven't seen a lot of activity on the React side for this (facebook/react#24430). We are stuck for the moment.

@wacanam
Copy link

wacanam commented Nov 23, 2022

I also had Hydration Error when I enable lastpass extension.

I tried to moved back to the old pages folder and the error was gone.
I used Nextjs version is 13.0.4 and mui 5.10.15

@JAD3N
Copy link

JAD3N commented Nov 28, 2022

I've made a repo with a blend of the Chakra-UI & Emotion examples provided by Remix with a workaround that fixes the issue where styles are missing on client-side rendering if hydration fails. The only major change is that I added the client context from the Emotion example and adjusted the root.tsx to re-inject the styles.

Repo: https://github.com/JAD3N/remix-chakra-ui

@adamzerner
Copy link

adamzerner commented Jan 11, 2023

It happens mostly in Remix because Remix renders/hydrates the whole HTML, while others render/hydrate only a < div id=root />

Given this, would it be a crazy idea to hydrate from a <div id="root">? Ie. in root.tsx have:

<html lang="en">
  <head>
    <Meta />
    <Links />
  </head>
  <body>
    <div id="root">
      <Outlet />
      <ScrollRestoration />
      <Scripts />
      <LiveReload />
    </div>
  </body>
</html>

And in entry.client.tsx have:

import { RemixBrowser } from '@remix-run/react';
import { hydrate } from 'react-dom';

hydrate(<RemixBrowser />, document.getElementById('root'));

The only issue I see with this is that you don't get the functionality of Meta and Links, but that seems easy enough to work around. My main worry is if this is a bad idea for other reasons that I'm not seeing and that are hard to anticipate in advance.

To work around Meta, this useMetaTitle hook seems to be working:

export const useMetaTitle = (title: string) => {
  useEffect(() => {
    const $title = document.querySelector('title');

    if ($title) {
      $title.innerText = title;
    }
  }, [title]);
};

Update: I tried hydrating from the div and so far it has solved the problem for everyone who has reported issues and hasn't lead to any other bugs.

@MatthieuCoelho
Copy link

With the new "defer" feature of Remix, it's seems important to solve this issue because "defer" can't be used without hydrateRoot.

@zolzaya
Copy link

zolzaya commented Jan 26, 2023

@adamzerner Hey what remix version are you using? I have a 1.11.1 version.

@adamzerner
Copy link

@zolzaya @remix-run/express, @remix-run/node and @remix-run/react are all on 1.7.2.

@rgmvisser
Copy link

Has anyone tried this solution? https://github.com/kiliman/remix-hydration-fix

@sanelaxm
Copy link

sanelaxm commented May 9, 2023

It happens mostly in Remix because Remix renders/hydrates the whole HTML, while others render/hydrate only a < div id=root />

Given this, would it be a crazy idea to hydrate from a <div id="root">? Ie. in root.tsx have:

<html lang="en">
  <head>
    <Meta />
    <Links />
  </head>
  <body>
    <div id="root">
      <Outlet />
      <ScrollRestoration />
      <Scripts />
      <LiveReload />
    </div>
  </body>
</html>

And in entry.client.tsx have:

import { RemixBrowser } from '@remix-run/react';
import { hydrate } from 'react-dom';

hydrate(<RemixBrowser />, document.getElementById('root'));

The only issue I see with this is that you don't get the functionality of Meta and Links, but that seems easy enough to work around. My main worry is if this is a bad idea for other reasons that I'm not seeing and that are hard to anticipate in advance.

To work around Meta, this useMetaTitle hook seems to be working:

export const useMetaTitle = (title: string) => {
  useEffect(() => {
    const $title = document.querySelector('title');

    if ($title) {
      $title.innerText = title;
    }
  }, [title]);
};

Update: I tried hydrating from the div and so far it has solved the problem for everyone who has reported issues and hasn't lead to any other bugs.

Can you show an example of usage of this hook? How is data retrieved from the meta function?

@brophdawg11 brophdawg11 self-assigned this Aug 3, 2023
@brophdawg11
Copy link
Contributor

Closing as a dup of #4822

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
No open projects
Status: Closed
Development

No branches or pull requests