Skip to content
This repository has been archived by the owner on Sep 27, 2022. It is now read-only.

Commit

Permalink
fix: emotion bug (#230)
Browse files Browse the repository at this point in the history
🙈 Fix `<style>` elements bleeding into the document `<body>`, a place where they [do not belong](mui/material-ui#26561 (comment))
ℹ️ Removed Remix/React control of the entire document to drastically simplify emotion logic. This also means the Remix `meta`/`link` features no longer work -> [react-helmet](https://github.com/nfl/react-helmet)

Closes #221
  • Loading branch information
3x071c authored Apr 23, 2022
1 parent cbfae9f commit cd6796d
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 147 deletions.
27 changes: 5 additions & 22 deletions app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,13 @@
import type { PropsWithChildren } from "react";
import { CacheProvider } from "@emotion/react";
import { useMemo, useState } from "react";
import { hydrate } from "react-dom";
import { RemixBrowser } from "remix";
import { createEmotionCache, EmotionClientContext } from "~app/emotion";
import { createEmotionCache } from "~app/emotion";

function ClientWrapper({ children }: PropsWithChildren<unknown>) {
const [cache, setCache] = useState(createEmotionCache());

const ctx = useMemo(
() => ({
reset: () => setCache(createEmotionCache()),
}),
[],
);

return (
<EmotionClientContext.Provider value={ctx}>
<CacheProvider value={cache}>{children}</CacheProvider>
</EmotionClientContext.Provider>
);
}
const cache = createEmotionCache();

hydrate(
<ClientWrapper>
<CacheProvider value={cache}>
<RemixBrowser />
</ClientWrapper>,
document,
</CacheProvider>,
document.getElementById("__remix"),
);
81 changes: 49 additions & 32 deletions app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable react/jsx-no-constructed-context-values */
import type { EntryContext } from "remix";
import { CacheProvider } from "@emotion/react";
import createEmotionServer from "@emotion/server/create-instance";
import { renderToString } from "react-dom/server";
import { RemixServer } from "remix";
Expand All @@ -8,48 +9,64 @@ import {
getColorModeCookie,
getInitialColorModeCookie,
} from "~app/colormode";
import { createEmotionCache, EmotionServerContext } from "~app/emotion";

const cache = createEmotionCache(); // @todo Figure out if global style caching is a good idea
// eslint-disable-next-line @typescript-eslint/unbound-method
const { extractCriticalToChunks } = createEmotionServer(cache);
import { createEmotionCache } from "~app/emotion";

export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
): Response {
const markup = (
<ColorModeContext.Provider
value={{
current: getColorModeCookie(
request.headers.get("Cookie") || "",
),
initial: getInitialColorModeCookie(
request.headers.get("Cookie") || "",
),
}}>
<RemixServer context={remixContext} url={request.url} />
</ColorModeContext.Provider>
);
const cache = createEmotionCache();
// eslint-disable-next-line @typescript-eslint/unbound-method
const { extractCriticalToChunks, constructStyleTagsFromChunks } =
createEmotionServer(cache);

const prerender = renderToString(
<EmotionServerContext.Provider value={null}>
{markup}
</EmotionServerContext.Provider>,
const html = renderToString(
<CacheProvider value={cache}>
<ColorModeContext.Provider
value={{
current: getColorModeCookie(
request.headers.get("Cookie") || "",
),
initial: getInitialColorModeCookie(
request.headers.get("Cookie") || "",
),
}}>
<RemixServer context={remixContext} url={request.url} />
</ColorModeContext.Provider>
</CacheProvider>,
);

const chunks = extractCriticalToChunks(prerender);
const render = renderToString(
<EmotionServerContext.Provider value={chunks.styles}>
{markup}
</EmotionServerContext.Provider>,
);
const chunks = extractCriticalToChunks(html);
const styles = constructStyleTagsFromChunks(chunks);

responseHeaders.set("Content-Type", "text/html");
return new Response(`<!DOCTYPE html>${render}`, {
headers: responseHeaders,
status: responseStatusCode,
});
return new Response(
`<!DOCTYPE html>
<html lang="de">
<head>
${styles}
<meta charSet="utf-8" />
<meta
name="viewport"
content="width=device-width,initial-scale=1"
/>
<title>LSG</title>
</head>
<body>
<div id="__remix">`
.trim()
.replaceAll(/\s+/g, " ") +
html +
`</div>
</body>
</html>`
.trim()
.replaceAll(/\s+/g, " "),
{
headers: responseHeaders,
status: responseStatusCode,
},
);
}
130 changes: 37 additions & 93 deletions app/root.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,27 @@
/* eslint-disable no-console */
import type { StyleSheet } from "@emotion/utils";
import type { PropsWithChildren } from "react";
import type { MetaFunction, LoaderFunction } from "remix";
import { Center, chakra, Heading, Text, Code } from "@chakra-ui/react";
import { withEmotionCache } from "@emotion/react";
import { memo, useContext, useEffect } from "react";
import type { LoaderFunction } from "remix";
import {
Center,
chakra,
Heading,
Text,
Code,
ChakraProvider,
} from "@chakra-ui/react";
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useCatch,
} from "remix";
import { ColorModeManager, ColorModeToggle } from "~app/colormode";
import { EmotionServerContext, EmotionClientContext } from "~app/emotion";
import { ColorModeToggle } from "~app/colormode";
import { theme } from "~feat/chakra";
import { LinkButton } from "~feat/links";
import { respond, useLoaderResponse } from "~lib/response";
import { keys } from "~lib/util";

export const meta: MetaFunction = () => {
return { title: "LSG" };
};

type LoaderData = {
env: {
MAGIC_KEY: string | undefined;
Expand All @@ -49,84 +47,30 @@ declare global {
}
}

const Document = memo(
withEmotionCache(function InnerDocument(
{
children,
title,
env,
}: PropsWithChildren<{ title?: string; env?: LoaderData["env"] }>,
emotionCache,
) {
const emotionServerContext = useContext(EmotionServerContext);
const emotionClientContext = useContext(EmotionClientContext);

// Only executed on client, when Document is re-mounted (error boundary)
useEffect(() => {
// re-link sheet container
// eslint-disable-next-line no-param-reassign
emotionCache.sheet.container = document.head;

// re-inject tags
const { tags } = emotionCache.sheet;
emotionCache.sheet.flush();
tags.forEach((tag) => {
// eslint-disable-next-line no-underscore-dangle -- External, Private API
(
emotionCache.sheet as unknown as {
_insertTag: (
tag: StyleSheet["tags"][number],
) => unknown;
}
)._insertTag(tag);
});

// reset cache to re-apply global styles
return emotionClientContext?.reset();
// eslint-disable-next-line react-hooks/exhaustive-deps -- Only trigger on remount
}, []);

return (
<html lang="de">
<head>
{emotionServerContext?.map(({ key, ids, css }) => (
<style
key={key}
data-emotion={`${key} ${ids.join(" ")}`}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: css }}
/>
))}
<meta charSet="utf-8" />
<meta
name="viewport"
content="width=device-width,initial-scale=1"
/>
{title ? <title>{title}</title> : null}
<Meta />
<Links />
</head>
<body>
<ColorModeManager>
<ColorModeToggle />
{children}
</ColorModeManager>
<ScrollRestoration />
<Scripts />
<LiveReload />
{env && (
<script
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: `window.env = ${JSON.stringify(env)}`,
}}
/>
)}
</body>
</html>
);
}),
);
const Document = function InnerDocument({
children,
env,
}: PropsWithChildren<{ env?: LoaderData["env"] }>) {
return (
<>
<ChakraProvider theme={theme}>
<ColorModeToggle />
{children}
</ChakraProvider>
<ScrollRestoration />
<Scripts />
<LiveReload />
{env && (
<script
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: `window.env = ${JSON.stringify(env)}`,
}}
/>
)}
</>
);
};

export default function App(): JSX.Element {
const { env } = useLoaderResponse<LoaderData>();
Expand Down Expand Up @@ -154,7 +98,7 @@ export function CatchBoundary(): JSX.Element {
: "Unbekannter Fehler - Bei wiederholtem, unvorhergesehenen Auftreten bitte melden 🤯";

return (
<Document title={`${status} | LSG`}>
<Document>
<Center minW="100vw" minH="100vh">
<chakra.main p={2} textAlign="center">
<Heading as="h1" size="xl">
Expand All @@ -180,7 +124,7 @@ export function ErrorBoundary({ error }: { error: Error }): JSX.Element {
const { name, message } = error;

return (
<Document title={`${name} | LSG`}>
<Document>
<Center minW="100vw" minH="100vh">
<chakra.main p={2} textAlign="center">
<Heading as="h1" size="xl">
Expand Down

0 comments on commit cd6796d

Please sign in to comment.