Deploy React Router to the edge
A React Router app in framework mode deploys in one of two ways:
- Rendered on the server, when it's built with Cloudflare's Vite plugin: loaders and actions run in a Worker on Cloudflare's network, next to your static files. See Server rendering.
- As a single-page app (
ssr: false), served as static files. Routes you list inprerenderalso get their own HTML, rendered at build time. See Single-page mode.
A Vite app that only uses the react-router library for routing isn't concerned: it deploys as a Vite app.
Detection
A project is treated as React Router when @react-router/dev is in dependencies or devDependencies, or when a react-router.config.ts, .js, .mjs or .mts file sits at its root. React Router is checked before Vite, which it runs on.
Defaults
| Setting | Default |
|---|---|
| Install command | From your lockfile, see Frameworks |
| Build command | npx react-router build |
| Output directory | build/client |
Keep isbot in your dependencies, as React Router's templates do: a build that doesn't find it installs it on its own, changing your package.json in the middle of the build.
Server rendering
React Router's Cloudflare template renders on the server with @cloudflare/vite-plugin. Start from it:
Or add the plugin to an existing app, before reactRouter(), with the ssr environment:
Its Worker entry (main in wrangler.jsonc, workers/app.ts in the template) hands requests to React Router. The build command doesn't change: react-router build writes the Worker and your static files, and Hiraiship publishes both. The output directory setting is ignored, since the build says where everything is.
Static files are served without running your Worker. Every other path runs it, unknown ones included: your root route's error boundary renders the 404.
A server build without the plugin (React Router's default Node server) fails with an error naming the plugin to add.
Reading variables
Your runtime variables reach the Worker as bindings. Read them from cloudflare:workers, as the template does:
On a project whose latest deployment rendered on the server, a new variable in the dashboard is both a build and a runtime variable by default. A secret is runtime only, so it never ends up in your bundle.
vars in wrangler.jsonc are ignored, and the build log says so: set those values as the project's variables instead. .dev.vars isn't read either.
What isn't supported
Hiraiship provides your Worker with its variables and its static files, nothing else. A build whose wrangler.jsonc declares other bindings fails, naming each one: KV namespaces, D1 databases, R2 buckets, Durable Objects, queues, service bindings, Workers AI, Hyperdrive, Analytics Engine, cron triggers. A Worker that read env.DB and got nothing would fail on every request.
WebAssembly, text and data modules aren't supported either: the Worker must be JavaScript only.
CPU time
Each server-rendered request runs your Worker, within your plan's CPU time per request:
| Free | Standard | Pro | Platform | Enterprise | |
|---|---|---|---|---|---|
| CPU per request | 15 ms | 30 ms | 50 ms | 50 ms | 50 ms |
That's enough for a typical page. A heavy render that goes past it fails with Cloudflare's exceededResources error. See Limits.
Single-page mode
To deploy without a server, set ssr: false in your config:
The build then writes an index.html to build/client, and every path with no file of its own gets it, so the router takes over in the browser: opening or reloading /settings works.
With ssr: true, React Router's default, and no Cloudflare plugin, the build fails with an error rather than publishing pages that would never render, prerendered ones included.
React Router still renders your root route once at build time to write index.html, so it must not use browser-only APIs (window, localStorage) during its first render. And only prerendered routes can have a loader: use clientLoader in the others.
Prerendering
Routes listed in prerender get their own HTML at build time, served as is to visitors and search engines; the others stay client-rendered:
Environment variables
These are the build variables every React Router app has. A server-rendered app also reads runtime variables, see Reading variables.
React Router builds with Vite: import.meta.env.VITE_* values are inlined into your bundle at build time. Add them as build variables: in the dashboard, a new variable on a single-page React Router project is a build variable by default.
Only VITE_-prefixed variables reach your code. Whatever ends up in the bundle is downloaded by every visitor, which is why a secret can't be a build variable.
For one deployment only, pass them to the CLI:
Paths with no file
In single-page mode, unknown paths get your index.html, and React Router renders the matching route, or its own not-found boundary, in the browser.
When / is in prerender, index.html is your prerendered home page, and React Router writes the single-page shell to __spa-fallback.html instead. Hiraiship then serves that shell as 404.html: a path with no file of its own still opens your app at the right route, but answers with a 404 status. To keep 200s on client-rendered routes, leave / out of prerender, or prerender every route. A 404.html of your own is never replaced. See Static sites.