Created
December 1, 2022 17:19
-
-
Save brophdawg11/d927e4d3d6035cea2fa6c37924118edd to your computer and use it in GitHub Desktop.
Remix without managing the full document (Express Template)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { RemixBrowser } from "@remix-run/react"; | |
import { startTransition, StrictMode } from "react"; | |
import { hydrateRoot } from "react-dom/client"; | |
function hydrate() { | |
startTransition(() => { | |
hydrateRoot( | |
// π Hydrate into the proper element, not the document! | |
document.querySelector("#app"), | |
<StrictMode> | |
<RemixBrowser /> | |
</StrictMode> | |
); | |
}); | |
} | |
if (window.requestIdleCallback) { | |
window.requestIdleCallback(hydrate); | |
} else { | |
// Safari doesn't support requestIdleCallback | |
// https://caniuse.com/requestidlecallback | |
window.setTimeout(hydrate, 1); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import type { EntryContext } from "@remix-run/node"; | |
import { Response } from "@remix-run/node"; | |
import { RemixServer } from "@remix-run/react"; | |
import { renderToString } from "react-dom/server"; | |
export default function handleRequest( | |
request: Request, | |
responseStatusCode: number, | |
responseHeaders: Headers, | |
remixContext: EntryContext | |
) { | |
// π Return the sub-document HTML directly | |
let markup = renderToString( | |
<RemixServer context={remixContext} url={request.url} /> | |
); | |
responseHeaders.set("Content-Type", "text/html"); | |
return new Response(markup, { | |
status: responseStatusCode, | |
headers: responseHeaders, | |
}); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Link, Outlet, Scripts } from "@remix-run/react"; | |
// Root component should just return a subset of HTML, not an <html> document | |
export default function App() { | |
return ( | |
<div> | |
<nav> | |
<Link to="/">Home</Link> | |
<Link to="/page">Page</Link> | |
</nav> | |
<Outlet /> | |
<Scripts /> | |
</div> | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const path = require("path"); | |
const express = require("express"); | |
const compression = require("compression"); | |
const morgan = require("morgan"); | |
const { | |
AbortController: NodeAbortController, | |
createRequestHandler: createRemixRequestHandler, | |
Headers: NodeHeaders, | |
Request: NodeRequest, | |
} = require("@remix-run/node"); | |
const BUILD_DIR = path.join(process.cwd(), "build"); | |
const app = express(); | |
app.use(compression()); | |
// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header | |
app.disable("x-powered-by"); | |
// Remix fingerprints its assets so we can cache forever. | |
app.use( | |
"/build", | |
express.static("public/build", { immutable: true, maxAge: "1y" }) | |
); | |
// Everything else (like favicon.ico) is cached for an hour. You may want to be | |
// more aggressive with this caching. | |
app.use(express.static("public", { maxAge: "1h" })); | |
app.use(morgan("tiny")); | |
app.all("*", async (req, res, next) => { | |
if (process.env.NODE_ENV === "development") { | |
purgeRequireCache(); | |
} | |
let handler = createRequestHandler({ | |
build: require(BUILD_DIR), | |
mode: process.env.NODE_ENV, | |
}); | |
let nodeResponse = await handler(req, res, next); | |
// π These are basically the inlined contents of `sendRemixResponse` so we | |
// can manually send our HTML document | |
res.statusMessage = nodeResponse.statusText; | |
res.status(nodeResponse.status); | |
for (let [key, values] of Object.entries(nodeResponse.headers.raw())) { | |
for (let value of values) { | |
res.append(key, value); | |
} | |
} | |
// π Grab the sub-document HTML and put it inside our own document | |
let contents = await nodeResponse.text(); | |
let html = `<html> | |
<head> | |
<title>Look ma sub-document hydration!</title> | |
</head> | |
<body> | |
<div id="app">${contents}</div> | |
</body> | |
</html>`; | |
res.send(html); | |
}); | |
const port = process.env.PORT || 3000; | |
app.listen(port, () => { | |
console.log(`Express server listening on port ${port}`); | |
}); | |
function purgeRequireCache() { | |
// purge require cache on requests for "server side HMR" this won't let | |
// you have in-memory objects between requests in development, | |
// alternatively you can set up nodemon/pm2-dev to restart the server on | |
// file changes, but then you'll have to reconnect to databases/etc on each | |
// change. We prefer the DX of this, so we've included it for you by default | |
for (const key in require.cache) { | |
if (key.startsWith(BUILD_DIR)) { | |
delete require.cache[key]; | |
} | |
} | |
} | |
// Slightly altered function from @remix-run/express | |
function createRequestHandler({ | |
build, | |
getLoadContext, | |
mode = process.env.NODE_ENV, | |
}) { | |
let handleRequest = createRemixRequestHandler(build, mode); | |
return async (req, res, next) => { | |
try { | |
let request = createRemixRequest(req, res); | |
let loadContext = getLoadContext?.(req, res); | |
let response = await handleRequest(request, loadContext); | |
// π Minor change from the built-in createRequestHandler. We don't want | |
// to write the response through `res` since we need to wrap our document | |
// shell around it still! Just return the fetch Response directly | |
return response; | |
} catch (error) { | |
console.log("Error!", error); | |
// Express doesn't support async functions, so we have to pass along the | |
// error manually using next(). | |
next(error); | |
} | |
}; | |
} | |
// Untouched function from @remix-run/express | |
function createRemixHeaders(requestHeaders) { | |
let headers = new NodeHeaders(); | |
for (let [key, values] of Object.entries(requestHeaders)) { | |
if (values) { | |
if (Array.isArray(values)) { | |
for (let value of values) { | |
headers.append(key, value); | |
} | |
} else { | |
headers.set(key, values); | |
} | |
} | |
} | |
return headers; | |
} | |
// Untouched function from @remix-run/express | |
function createRemixRequest(req, res) { | |
let origin = `${req.protocol}://${req.get("host")}`; | |
let url = new URL(req.url, origin); | |
// Abort action/loaders once we can no longer write a response | |
let controller = new NodeAbortController(); | |
res.on("close", () => controller.abort()); | |
let init = { | |
method: req.method, | |
headers: createRemixHeaders(req.headers), | |
// Cast until reason/throwIfAborted added | |
// https://github.com/mysticatea/abort-controller/issues/36 | |
signal: controller.signal, | |
}; | |
if (req.method !== "GET" && req.method !== "HEAD") { | |
init.body = req; | |
} | |
return new NodeRequest(url.href, init); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment