Documentation Index
Fetch the complete documentation index at: https://mintlify.com/remix-run/react-router/llms.txt
Use this file to discover all available pages before exploring further.
Server Rendering
React Router Framework Mode includes built-in server-side rendering (SSR) support through the Vite plugin.
Overview
SSR is enabled by default in Framework Mode. The Vite plugin handles:
- Rendering React components to HTML on the server
- Loading data via route
loader functions before rendering
- Hydrating the client-side application
- Handling navigation on both server and client
Entry Files
Server Entry
Create app/entry.server.tsx:
import { renderToString } from "react-dom/server";
import { ServerRouter } from "react-router";
import type { EntryContext } from "react-router";
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
routerContext: EntryContext,
) {
const html = renderToString(
<ServerRouter
context={routerContext}
url={request.url}
/>
);
return new Response("<!DOCTYPE html>" + html, {
status: responseStatusCode,
headers: {
"Content-Type": "text/html",
...Object.fromEntries(responseHeaders),
},
});
}
Client Entry
Create app/entry.client.tsx:
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter />
</StrictMode>
);
});
SSR Configuration
Configure SSR in react-router.config.ts:
export default {
// Enable/disable SSR (default: true)
ssr: true,
// Server module format (default: "esm")
serverModuleFormat: "esm", // or "cjs"
// Server build output file (default: "index.js")
serverBuildFile: "index.js",
} satisfies Config;
Development Server
The dev server handles SSR automatically:
The Vite plugin:
- Intercepts incoming requests
- Loads the server build
- Executes route
loader functions
- Renders the React tree to HTML
- Returns the HTML response
Production Server
Using @react-router/serve
The simplest way to run your SSR app:
npx react-router build
npx react-router-serve ./build/server/index.js
Custom Server
Create a custom server with Express:
import express from "express";
import { createRequestHandler } from "@react-router/express";
import * as build from "./build/server/index.js";
const app = express();
// Serve static assets
app.use(
"/assets",
express.static("build/client/assets", {
immutable: true,
maxAge: "1y",
})
);
app.use(express.static("build/client", { maxAge: "1h" }));
// SSR request handler
app.all("*", createRequestHandler({ build }));
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
});
Node.js Adapter
Convert Node.js requests to Web Fetch API:
import { fromNodeRequest } from "@react-router/node";
import type { RequestHandler } from "react-router";
const handler: RequestHandler = async (request) => {
// Handle Web Request
return new Response("Hello");
};
// In your Node.js server
server.on("request", async (req, res) => {
const request = await fromNodeRequest(req, res);
const response = await handler(request);
// Send response back to Node.js
});
SPA Mode
Disable SSR for Single Page Application mode:
export default {
ssr: false,
} satisfies Config;
In SPA mode:
- Only the root route and
HydrateFallback are rendered at build time
- An
index.html file is generated
- All navigation happens client-side
- No server is needed in production
Server vs. Client Code
Server-Only Modules
Code that should only run on the server:
// utils/server.server.ts
import { db } from "~/db.server";
export async function getUser(id: string) {
return db.user.findUnique({ where: { id } });
}
The .server.ts suffix ensures this code is excluded from client bundles.
Client-Only Code
Use client-only imports:
import { useEffect, useState } from "react";
export default function Component() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
return <ClientOnlyComponent />;
}
Streaming SSR
Use React streaming APIs for better performance:
import { renderToPipeableStream } from "react-dom/server";
import { ServerRouter } from "react-router";
import type { EntryContext } from "react-router";
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
routerContext: EntryContext,
) {
return new Promise((resolve, reject) => {
let didError = false;
const { pipe } = renderToPipeableStream(
<ServerRouter
context={routerContext}
url={request.url}
/>,
{
onShellReady() {
const body = new PassThrough();
pipe(body);
resolve(
new Response(body as any, {
status: didError ? 500 : responseStatusCode,
headers: {
"Content-Type": "text/html",
...Object.fromEntries(responseHeaders),
},
})
);
},
onShellError(error) {
reject(error);
},
onError(error) {
didError = true;
console.error(error);
},
}
);
});
}
Middleware Mode
Integrate with existing servers:
import { defineConfig } from "vite";
import { reactRouter } from "@react-router/dev/vite";
export default defineConfig({
plugins: [reactRouter()],
server: {
middlewareMode: true,
},
});
Then in your server:
import express from "express";
import { createServer } from "vite";
const app = express();
const vite = await createServer({
server: { middlewareMode: true },
});
app.use(vite.middlewares);
Environment Variables
Environment variables are handled automatically:
// Server-side
export async function loader() {
const apiKey = process.env.API_KEY; // Available
return { data: "..." };
}
// Client-side
export default function Component() {
// Only VITE_* vars available here
const publicKey = import.meta.env.VITE_PUBLIC_KEY;
}
Server Build Output
After building, the server output is in build/server/:
build/
├── client/ # Client assets
│ ├── assets/ # Hashed JS/CSS
│ └── .vite/ # Vite manifest
└── server/ # Server build
└── index.js # Server entry point
Load Context
Pass server context to loaders:
// In your server
import { createRequestHandler } from "@react-router/express";
app.all(
"*",
createRequestHandler({
build,
getLoadContext(req, res) {
return {
user: req.user,
db: req.db,
};
},
})
);
Access in routes:
import type { LoaderFunctionArgs } from "react-router";
export async function loader({ context }: LoaderFunctionArgs) {
const user = context.user;
return { user };
}
See Also