Skip to content

Instantly share code, notes, and snippets.

@brophdawg11
Created December 1, 2022 17:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save brophdawg11/d927e4d3d6035cea2fa6c37924118edd to your computer and use it in GitHub Desktop.
Save brophdawg11/d927e4d3d6035cea2fa6c37924118edd to your computer and use it in GitHub Desktop.
Remix without managing the full document (Express Template)
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);
}
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,
});
}
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>&nbsp;
<Link to="/page">Page</Link>
</nav>
<Outlet />
<Scripts />
</div>
);
}
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