remix icon indicating copy to clipboard operation
remix copied to clipboard

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

Open giltayar opened this issue 2 years ago • 20 comments

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.

giltayar avatar Sep 11 '22 08:09 giltayar

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?

  • https://github.com/remix-run/remix/issues/2570
  • https://github.com/facebook/react/blob/main/CHANGELOG.md#1820-june-14-2022
  • https://github.com/facebook/react/pull/24523

tshddx avatar Sep 12 '22 22:09 tshddx

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

https://github.com/remix-run/remix/issues/2570#issuecomment-1186095298

tshddx avatar Sep 12 '22 22:09 tshddx

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

giltayar avatar Sep 13 '22 04:09 giltayar

Some notes to anybody arriving here:

  1. As @tshddx said, this is probably the same bug as https://github.com/remix-run/remix/issues/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)

giltayar avatar Sep 13 '22 05:09 giltayar

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: https://github.com/facebook/react/issues/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>
    );
  });
}

JAD3N avatar Oct 27 '22 11:10 JAD3N

react issue reference: https://github.com/facebook/react/issues/24430

KasparRosin avatar Nov 01 '22 12:11 KasparRosin

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>
    );
  });
}

tamm avatar Nov 03 '22 03:11 tamm

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

nicolaserny avatar Nov 09 '22 18:11 nicolaserny

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

KasparRosin avatar Nov 10 '22 09:11 KasparRosin

@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 avatar Nov 10 '22 09:11 nicolaserny

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

nicolaserny avatar Nov 10 '22 10:11 nicolaserny

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

MichaelDeBoey avatar Nov 20 '22 21:11 MichaelDeBoey

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?

dbashford avatar Nov 21 '22 16:11 dbashford

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.

dmarkow avatar Nov 21 '22 16:11 dmarkow

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 (https://github.com/facebook/react/issues/24430). We are stuck for the moment.

dbashford avatar Nov 21 '22 16:11 dbashford

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

wacanam avatar Nov 23 '22 13:11 wacanam

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

JAD3N avatar Nov 28 '22 19:11 JAD3N

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.

adamzerner avatar Jan 11 '23 21:01 adamzerner

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

MatthieuCoelho avatar Jan 19 '23 11:01 MatthieuCoelho

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

zolzaya avatar Jan 26 '23 07:01 zolzaya

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

adamzerner avatar Jan 30 '23 21:01 adamzerner

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

rgmvisser avatar Mar 21 '23 09:03 rgmvisser

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?

sanelaxm avatar May 09 '23 11:05 sanelaxm

Closing as a dup of https://github.com/remix-run/remix/issues/4822

brophdawg11 avatar Aug 09 '23 19:08 brophdawg11