From 4d3a784f254ba6c2a50eefab6a00819eafb2f8fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BD=D1=8F=20=D0=A1=D0=B0=D1=84=D0=B8=D0=BD?= Date: Sun, 11 Feb 2024 01:09:57 +0300 Subject: [PATCH 1/2] Update Chakra UI example. --- chakra-ui/app/emotion/context.tsx | 19 +++ chakra-ui/app/emotion/createEmotionCache.ts | 7 + chakra-ui/app/entry.client.tsx | 52 ++++--- chakra-ui/app/entry.server.tsx | 145 +++++--------------- chakra-ui/app/root.tsx | 111 +++++++-------- chakra-ui/package.json | 26 ++-- chakra-ui/remix.config.js | 5 +- chakra-ui/tsconfig.json | 7 +- 8 files changed, 154 insertions(+), 218 deletions(-) create mode 100644 chakra-ui/app/emotion/context.tsx create mode 100644 chakra-ui/app/emotion/createEmotionCache.ts diff --git a/chakra-ui/app/emotion/context.tsx b/chakra-ui/app/emotion/context.tsx new file mode 100644 index 00000000..e6a4abe3 --- /dev/null +++ b/chakra-ui/app/emotion/context.tsx @@ -0,0 +1,19 @@ +import { createContext } from "react"; + +export interface ServerStyleContextData { + key: string; + ids: Array; + css: string; +} + +export const ServerStyleContext = createContext< + ServerStyleContextData[] | null +>(null); + +export interface ClientStyleContextData { + reset: () => void; +} + +export const ClientStyleContext = createContext( + null +); diff --git a/chakra-ui/app/emotion/createEmotionCache.ts b/chakra-ui/app/emotion/createEmotionCache.ts new file mode 100644 index 00000000..f63a01da --- /dev/null +++ b/chakra-ui/app/emotion/createEmotionCache.ts @@ -0,0 +1,7 @@ +import createCache from "@emotion/cache"; + +export const defaultCache = createEmotionCache(); + +export default function createEmotionCache() { + return createCache({ key: "cha" }); +} diff --git a/chakra-ui/app/entry.client.tsx b/chakra-ui/app/entry.client.tsx index bb3edb2f..6c9c5206 100644 --- a/chakra-ui/app/entry.client.tsx +++ b/chakra-ui/app/entry.client.tsx @@ -1,28 +1,36 @@ -import createEmotionCache from "@emotion/cache"; -import { CacheProvider } from "@emotion/react"; import { RemixBrowser } from "@remix-run/react"; -import { startTransition, StrictMode } from "react"; +import React, { startTransition, StrictMode, useState } from "react"; +import { CacheProvider } from "@emotion/react"; import { hydrateRoot } from "react-dom/client"; -const hydrate = () => { - const emotionCache = createEmotionCache({ key: "css" }); +import createEmotionCache, { defaultCache } from "~/emotion/createEmotionCache"; +import { ClientStyleContext } from "~/emotion/context"; + +interface ClientCacheProviderProps { + children: React.ReactNode; +} + +function ClientCacheProvider({ children }: ClientCacheProviderProps) { + const [cache, setCache] = useState(defaultCache); - startTransition(() => { - hydrateRoot( - document, - - - - - , - ); - }); -}; + function reset() { + setCache(createEmotionCache()); + } -if (typeof requestIdleCallback === "function") { - requestIdleCallback(hydrate); -} else { - // Safari doesn't support requestIdleCallback - // https://caniuse.com/requestidlecallback - setTimeout(hydrate, 1); + return ( + + {children} + + ); } + +startTransition(() => { + hydrateRoot( + document, + + + + + + ); +}); diff --git a/chakra-ui/app/entry.server.tsx b/chakra-ui/app/entry.server.tsx index 60496c3a..2ecad50a 100644 --- a/chakra-ui/app/entry.server.tsx +++ b/chakra-ui/app/entry.server.tsx @@ -1,128 +1,49 @@ -import { PassThrough } from "stream"; +import type { AppLoadContext, EntryContext } from "@remix-run/node"; +import { renderToString } from "react-dom/server"; -import createEmotionCache from "@emotion/cache"; -import { CacheProvider as EmotionCacheProvider } from "@emotion/react"; +import { CacheProvider } from "@emotion/react"; +import createEmotionCache from "~/emotion/createEmotionCache"; import createEmotionServer from "@emotion/server/create-instance"; -import type { AppLoadContext, EntryContext } from "@remix-run/node"; -import { Response } from "@remix-run/node"; +import { ServerStyleContext } from "~/emotion/context"; import { RemixServer } from "@remix-run/react"; -import isbot from "isbot"; -import { renderToPipeableStream } from "react-dom/server"; - -const ABORT_DELAY = 5000; -const handleRequest = ( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - remixContext: EntryContext, - loadContext: AppLoadContext, -) => - isbot(request.headers.get("user-agent")) - ? handleBotRequest( - request, - responseStatusCode, - responseHeaders, - remixContext, - ) - : handleBrowserRequest( - request, - responseStatusCode, - responseHeaders, - remixContext, - ); -export default handleRequest; +const ABORT_DELAY = 5_000; -const handleBotRequest = ( +export default function handleRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, remixContext: EntryContext, -) => - new Promise((resolve, reject) => { - let didError = false; - const emotionCache = createEmotionCache({ key: "css" }); - - const { pipe, abort } = renderToPipeableStream( - + // This is ignored so we can keep it in the template for visibility. Feel + // free to delete this parameter in your app if you're not using it! + // eslint-disable-next-line @typescript-eslint/no-unused-vars + loadContext: AppLoadContext +) { + const cache = createEmotionCache(); + const { extractCriticalToChunks } = createEmotionServer(cache); + + const html = renderToString( + + - , - { - onAllReady: () => { - const reactBody = new PassThrough(); - const emotionServer = createEmotionServer(emotionCache); - - const bodyWithStyles = emotionServer.renderStylesToNodeStream(); - reactBody.pipe(bodyWithStyles); - - responseHeaders.set("Content-Type", "text/html"); - - resolve( - new Response(bodyWithStyles, { - headers: responseHeaders, - status: didError ? 500 : responseStatusCode, - }), - ); - - pipe(reactBody); - }, - onShellError: (error: unknown) => { - reject(error); - }, - onError: (error: unknown) => { - didError = true; + + + ); - console.error(error); - }, - }, - ); + const chunks = extractCriticalToChunks(html); - setTimeout(abort, ABORT_DELAY); - }); - -const handleBrowserRequest = ( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - remixContext: EntryContext, -) => - new Promise((resolve, reject) => { - let didError = false; - const emotionCache = createEmotionCache({ key: "css" }); - - const { pipe, abort } = renderToPipeableStream( - + const markup = renderToString( + + - , - { - onShellReady: () => { - const reactBody = new PassThrough(); - const emotionServer = createEmotionServer(emotionCache); - - const bodyWithStyles = emotionServer.renderStylesToNodeStream(); - reactBody.pipe(bodyWithStyles); - - responseHeaders.set("Content-Type", "text/html"); - - resolve( - new Response(bodyWithStyles, { - headers: responseHeaders, - status: didError ? 500 : responseStatusCode, - }), - ); - - pipe(reactBody); - }, - onShellError: (error: unknown) => { - reject(error); - }, - onError: (error: unknown) => { - didError = true; + + + ); - console.error(error); - }, - }, - ); + responseHeaders.set("Content-Type", "text/html"); - setTimeout(abort, ABORT_DELAY); + return new Response(`${markup}`, { + status: responseStatusCode, + headers: responseHeaders, }); +} diff --git a/chakra-ui/app/root.tsx b/chakra-ui/app/root.tsx index 6cbad662..51ba4771 100644 --- a/chakra-ui/app/root.tsx +++ b/chakra-ui/app/root.tsx @@ -1,5 +1,3 @@ -import { ChakraProvider, Box, Heading } from "@chakra-ui/react"; -import type { MetaFunction } from "@remix-run/node"; import { Links, LiveReload, @@ -7,41 +5,58 @@ import { Outlet, Scripts, ScrollRestoration, - useCatch, } from "@remix-run/react"; +import { withEmotionCache } from "@emotion/react"; +import React, { useContext, useEffect } from "react"; +import { ClientStyleContext, ServerStyleContext } from "~/emotion/context"; +import { ChakraProvider } from "@chakra-ui/react"; -export const meta: MetaFunction = () => ({ - charset: "utf-8", - viewport: "width=device-width,initial-scale=1", -}); +const Document = withEmotionCache( + ({ children }: { children: React.ReactNode }, emotionCache) => { + const serverStyleData = useContext(ServerStyleContext); + const clientStyleData = useContext(ClientStyleContext); -function Document({ - children, - title = "App title", -}: { - children: React.ReactNode; - title?: string; -}) { - return ( - - - - {title} - - - - {children} - - - - - - ); -} + // Only executed on client + useEffect(() => { + // re-link sheet container + emotionCache.sheet.container = document.head; + // re-inject tags + const tags = emotionCache.sheet.tags; + emotionCache.sheet.flush(); + tags.forEach((tag) => { + (emotionCache.sheet as any)._insertTag(tag); + }); + // reset cache to reapply global styles + clientStyleData?.reset(); + }, []); -export default function App() { - // throw new Error("💣💥 Booooom"); + return ( + + + + + + + {serverStyleData?.map(({ key, ids, css }) => ( +