remix: [Bug]: Style tags disappear from Head during Error/Catch Boundary
Which Remix packages are impacted?
remix(Remix core)@remix-run/react
What version of Remix are you using?
1.1.1
What version of Node are you using? Minimum supported version is 14.
16.13.0
Steps to Reproduce
Inject style tags into header in entry.server.tsx during markup generation. Example:
let markup = renderToString(
<RemixServer context={remixContext} url={request.url} />
);
const html = markup.replace(
"<head>",
`<head>${getSSRStyles(markup, server)}`
);
responseHeaders.set("Content-Type", "text/html");
return new Response("<!DOCTYPE html>" + html, {
status: responseStatusCode,
headers: responseHeaders,
});
Expected Behavior
The style tags should stay intact during 404, or at least after 404, refetch the HTML from server.entry.
I’m guessing because Remix only generates links an meta tags in head dynamically, things like style and base will get overridden when Error boundary generates a new document. This is unfortunate, as initial load will contain Style tags that prevent the page from loading janky and will be lost during errors.
This prevents us from using major UI libraries like Material UI and Mantine, which generate dynamic styles based on frameworks like emotion. The CatchBoundary/Error should re-render from server-side, not client-side (rehydrate head tag from server.entry)
Actual Behavior
Error/Catch boundaries removes everything in Head tag, not just the dynamically generated head-elements provided by the route, meaning style tags get removed, that are “global” styles.
About this issue
- Original URL
- State: closed
- Created 3 years ago
- Reactions: 48
- Comments: 62 (5 by maintainers)
Commits related to this issue
- feat(website): handle styles not appearing in root component This workaround for this bug https://github.com/remix-run/remix/issues/1136. — committed to IgnisDa/codefarem by IgnisDa 2 years ago
- Issue 23 (#24) * chore(utilities): change order in which languages appear * dev: install swift wasm in project * feat(swift-compiler): add basic swift support * ci(swift-compiler): add deplo... — committed to IgnisDa/codefarem by IgnisDa 2 years ago
@muco-rolle
I have A solution. It’s not ideal but it seems to work pretty well. (my solution is using styled-components, but I assume it should work with any css-in-js lib that adds styles to the <head> of the page)
routes/root.tsx<-- your root should have your<head>and<body>tags. You also shouldn’t have anyloaderoractionin this file. It also doesn’t hurt to setexport const unstable_shouldReload = () => false. The goal here is to make our root never re-render. Re-rendering is what causes all the styles to be removed (from my understanding). My root is pretty bare bones, it doesn’t have much in here.Now for the magic part. Pathless Layout Routes
Create a new pathless layout route that will be like your new “root”. I call mine
__mainor even__root. Do what makes sense to you.routes/__main<-- folder containing all your normal route modulesroutes/__main.tsx<-- this is almost like your new root route. My route component, it literally just a<Outlet />.The other important parts in your
routes/__main.tsxyou should have a CatchBoundary and ErrorBoundary.I also like to add a catch all route that I can force a 404. So my CatchBoundary in my __main and catch it.
routes/__main/$.tsx.I hope this all makes sense, but this is what I’ve been doing and so far I haven’t had any issues. I’m hoping to make a sample project showing this, but I haven’t gotten around to it yet.
So far I have ascertained a few things relating to this issue.
IMPORTANT
Navigating in browser to a non-existing url does NOT reproduce this issue,
entry.serverruns and produces correct results.INSTEAD
Clicking a link in app to a non-existing url renders
catchBoundarywithout runningentry.severand thus styles generated in this step are not generated and passed to entry.client for insertion.As well as losing any user generated style tags from the head the Remix Context does not get loaded in
catchBoundary. Then when user clicks ‘back’ it is also missing from the previous (genuine) route too. Instead there is an empty<script></script>where context is usually generated.Just want to throw out a
GOTCHAthat kicked my butt with all of this last week.TL;DR: stay on React 17 for now.
All of my work arounds I’ve listed previously is to prevent the
<head>tag from being rehydrated/replaced because you’ll lose all of your styles.Insert React 18.2 and anyone that has a browser plugin that inserts anything into the page: https://github.com/facebook/react/issues/24430
The way React 18.2 currently hydrates the page, if anything that was generated from the server doesn’t match what React wants to hydrate, React throws everything out and re-renders the document. Which in remix includes throwing out the head and replacing it, which throws out styles.
My team worked hard making sure that what gets server rendered is the same as what gets hydrated. Even with complicated things like localization, dates in different time zones, etc. Then we discovered if you have any browser extension that injects anything into the page (LastPass for example) this causes React 18.2 to re-render everything (because it will never match what we server rendered, and we can’t stop users from using plugins). Thus losing your styles.
This is also a problem with lots of automation tools that also inject scripts into the page while testing.
We feel very confident that in the future, either React will have a better solution for this, or even your CSS-in-JS libraries will have a solution to this. So we’re sticking with React 17 for now, we are considering going away from CSS-in-JS but we’ve got multiple years of component libraries and other products that are all built with CSS-in-JS, so not an easy switch by any means.
Hope this helps everyone in the meantime.
Had the same problem when trying to use Mantine. I was able to work around it by reapplying the styles on the client using emotion cache, it should also work for other libraries that use emotion but will probably need another context for server styles.
Made a small repo with an example, let me know if it helps @kwiat1990 @absamo
@muco-rolle
I saw this the other day, in theory it could. Although the library even mentions at the end of their readme that it may have unexpected results with libraries that inject things into the head like
styled-componentsand such.At my work, we’ve decided to move away from CSS-in-JS libraries and move towards solutions that give us css files to link on the page and not have the extra overhead of runtime css.
Hey @muco-rolle, I took a look at your working example, at it’s core, this is just like my solution and I’m glad it working out so far for you.
I only have a few suggestions that I hope you find helpful.
$.tsxI would suggest putting default export blank component in there. This is how remix will know if your route is a resource route or a regular one (without the default export, it thinks it’s a resource route). The reason for this, is when you use a Link Component to link to a non-existing page (like<Link to='/not-found'>), client side routing happens, tries to fetch data from your$.tsxloader, gets a 404 and knows to show yourCatchBoundary.If you do a hard-refresh of that same
/not-foundpage or even just type/whatever-you-want(any url to non-existing page) you’ll see you get a very different result when the page is first server-rendered. Remix thinks this is just a resource route, so it’ll server render it without any of the parent routes. If you place an empty default export, this will then help remix know to render all of the parents routes, thus being able to use your CatchBoundary.If you have components, providers, etc that don’t change, you can put those in your
routes/root.tsx. For example your<ChakraProvider>. That way you don’t have to put that in your CatchBoundary, ErrorBoundary and default export of your__main.tsx. Some of my apps have my navigation shell in myroutes/root.tsxfile. That way my Catch pages and Errors can still have the main navigation available to my users. But if that layout changes (maybe due to a user logged in or something), you’ll want that probably in your__main.tsx. If your theme for the ChakraProvider doesn’t ever change, then you can safely move it to theroutes/root.tsx.I highly suggest reading the docs about Module Constraints specifically about No Module Side Effects. In your
__main.tsxaround line 18, you have aconst theme = extendTheme({}). This causes a module side effect. If you don’t know what that is (I didn’t either until yesterday), the remix docss that I linked to a much better job explaining them than I could. Although what you current have will work and likely be totally fine, it’s good to understand module side effects because they can totally cause you issues (again, read that section, it’s good info).I’m not very familiar with Chakra UI, but it looks it uses Emotion under the hood to do it’s styling. I suggest taking a look at the Emotion example that remix has. This will help you get things set up so styles will also be server rendered. Your example now only does client side rendering (Emotions default), which will potentially show you a flash of unstyled content until emotions client side styling can happen.
Hope this helps! Happy coding
I’m experiencing a similar issue, and I was able to resolve component errors by doing what @akomm mentioned. However, even with using a CatchBoundary in the pathless route, 404s are still being unhandled, I’m assuming since they are not originating inside the pathless route, any way to handle that?
Any idea of how to resolve this? This is one of the last pieces preventing us from converting our apps at work to Remix.
Thanks for raising this issue, and thanks everyone for helping out with all the different fixes and workarounds.
Since this affects multiple libraries and the workaround for each one is going to be different, I’m going to close this overarching issue just so it’s not open indefinitely. If there are more specific issues related to a single CSS-in-JS library, UI framework etc., please feel free to open a separate issue.
Transparently supporting any library that injects global side-effects into the document would require a bigger change to Remix’s architecture. This is something we’ll keep in mind moving forwards but we don’t have any concrete plans to address this at the moment.
In the meantime I’d encourage everyone to help contribute to the Remix examples repo with any fixes or workarounds for specific libraries. Note that we already have a working Emotion example and I’ve just opened a PR that fixes the Mantine example based on the comments in this issue (thanks @correiarmjoao!).
I have not personally used Vanilla Extract. Based off of everything I’ve seen and read, I’d recommend it. Just about any css framework/tool that gives you a standard css file that you can link to is recommended by me. As @machour mentioned above, there is an example for remix as well.
@muco-rolle should work fine, we have an example in the examples repo. Not the ideal implementation, but it’s done like tailwind so you shouldn’t see the problem raised in this issue.
@jca41 if you want to avoid that, then yes having higher level Catch/Error Boundary will help. Typically I have multiple levels of Catch/Error Boundaries in my apps. I usually want to try to keep as much navigation and other elements on the page so if something does go wrong, the user still can navigate to other sections of the app easily.
Can you please share an example repo where you implemented this?
Thanks @kevinbailey25 I’ve updated my comment with the $.tsx moved into __boundary
I’d like to reiterate the solution that @kevinbailey25 provided with what I discovered:
ErrorBoundaryandCatchBoundarytorootwill not work if you use any form of styling (~ everyone?)So the fix is to create a
__boundarywhich is used to export these functions using a file structure of:<Outlet />component, and the__boundaryshould only export<Outlet />. If any other components are placed inside__boundarythey will render twice - once whenrootis rendered, then again when picked up by the pathless (__) route.With this approach everything works as expected, although a) this should be clarified on the Routing page or better yet it would be great if b) Remix could create a virtual boundary within
rootso that theErrorBoundaryandCatchBoundarycould be placed there instead of needing this pathless workaround.I’m also experiencing this, and can second @jpod32’s experience. I tried the pathless route solution and had the same issue for 404s from invalid URLs.
Has anyone else found a workaround to this yet?
Thank you @akomm! This should do exactly what I need. I knew about the pathless routes in
react-routerbut didn’t know about the double underscore convention in remix.@kevinbailey25
Add pathless layout route Add ErrorBoundary and CatchBoundary in this pathless-layout-routes
Add the pathless route directly descending your
rootroute.Now the issue should only appear, if you have an error in your pathless route, but not any route beneath it.
Just to add you can’t hit the “back” button in this scenario either, it will throw another error around appendingChild as the loader will run but have no context anymore (I assume)