remix
remix copied to clipboard
Interaction between Remix, Lastpass, and <link rel=stylesheet> and (possibly) @import
What version of Remix are you using?
1.7.0
Steps to Reproduce
- Install Lastpass (I think you should also be registered...?)
-
git clone https://github.com/giltayar/remix-import-css-lastpass-hydration-error-reproduction
-
npm install
-
npm run dev
- This should show the page with no errors in console
Now...
- Edit
root.tsx
and comment in the<link rel=stylesheet>
.
Or...
- 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:
- Whether Chrome or Firefox
- Whether devtools is open
- Whether "disable cache" is turned on
- Whether using
links
function,link rel
inhead
, or using@import
.
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
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
Yes, we're using the latest remix and React 18.2.0.
Some notes to anybody arriving here:
- As @tshddx said, this is probably the same bug as https://github.com/remix-run/remix/issues/2570
- It is caused by Chrome/FF extensions that mutate the DOM (probably before React has a chance to hydrate)
- It happens mostly in Remix because Remix renders/hydrates the whole HTML, while others render/hydrate only a
<div id=root />
- 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. -
There is a workaround: replace
hydrateRoot
with pre-React 18hydrate
(in Remix, this isentry.client.tsx
)
I was able to reproduce this by:
- Using Firefox (tried on Chrome but it worked?) install the LastPass addon.
- Log in to LastPass addon otherwise it works.
- Use the Chakra UI template (which uses Emotion):
npx create-remix@latest --template examples/chakra-ui
- Add a font import:
npm i @fontsource/inter
- Add links export to root layout:
import fontStyles from "@fontsource/inter/variable.css";
export const links: LinksFunction = () => [
{
rel: "stylesheet",
href: fontStyles,
},
];
- 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>
);
});
}
react issue reference: https://github.com/facebook/react/issues/24430
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>
);
});
}
Some notes to anybody arriving here:
- 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
- It is caused by Chrome/FF extensions that mutate the DOM (probably before React has a chance to hydrate)
- It happens mostly in Remix because Remix renders/hydrates the whole HTML, while others render/hydrate only a
<div id=root />
- 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.- There is a workaround: replace
hydrateRoot
with pre-React 18hydrate
(in Remix, this isentry.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 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, 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.
@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.
This is probably because one of your browser extensions is injecting code : https://remix.run/pages/gotchas#browser-extensions-injecting-code
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?
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.
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.
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
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
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.
With the new "defer" feature of Remix, it's seems important to solve this issue because "defer" can't be used without hydrateRoot.
@adamzerner Hey what remix version are you using? I have a 1.11.1
version.
@zolzaya @remix-run/express
, @remix-run/node
and @remix-run/react
are all on 1.7.2
.
Has anyone tried this solution? https://github.com/kiliman/remix-hydration-fix
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. inroot.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
andLinks
, 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
, thisuseMetaTitle
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?
Closing as a dup of https://github.com/remix-run/remix/issues/4822