1---
2title: React Router
3description: Deploy React Router v7 — including React Server Components — to Cloudflare with Cloudflare.Website.Vite and viteEnvironments.
4sidebar:
5 order: 6
6---
7
8import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
9
10export const deps = `react react-dom react-router`;
11export const devDeps = `vite @vitejs/plugin-react @vitejs/plugin-rsc`;
12
13[React Router](https://reactrouter.com) v7 builds through Vite, so
14[`Cloudflare.Website.Vite`](/cloudflare/frontend/vite) deploys it — one
15resource, no Wrangler config, no manual entrypoint. In React Server
16Components mode the build emits **multiple server environments** (`rsc`
17and `ssr`) instead of the single `ssr` environment most SSR frameworks
18produce, and `viteEnvironments` is the prop that tells Alchemy how those
19environments assemble into one Worker. The configuration on this page is
20a supported, live-tested shape: Alchemy's test suite deploys it to real
21Cloudflare and asserts that server-rendered HTML and client routes both
22serve.
23
24## Project layout
25
26React Router's RSC mode is wired directly on `@vitejs/plugin-rsc`, so
27the per-environment entry modules live in your project:
28
29```
30.
31├── alchemy.run.ts # the Stack
32├── vite.config.ts
33├── app/ # root.tsx, routes.ts, routes/
34└── react-router-vite/ # entry.browser.tsx, entry.ssr.tsx,
35 # entry.worker.tsx, worker-ssr.tsx
36```
37
38`app/` holds ordinary React Router routes; `react-router-vite/` holds
39one entry module per Vite environment — the browser bundle, the SSR
40renderer, and the Worker handler.
41
42## Install the dependencies
43
44React Server Components need React Router 7.16+ and the RSC Vite
45plugin:
46
47<Tabs syncKey="pkgManager">
48 <TabItem label="bun" icon="bun">
49 <Code code={`bun add ${deps}\nbun add -d ${devDeps}`} lang="sh" />
50 </TabItem>
51 <TabItem label="npm" icon="npm">
52 <Code code={`npm install ${deps}\nnpm install -D ${devDeps}`} lang="sh" />
53 </TabItem>
54 <TabItem label="pnpm" icon="pnpm">
55 <Code code={`pnpm add ${deps}\npnpm add -D ${devDeps}`} lang="sh" />
56 </TabItem>
57 <TabItem label="yarn" icon="seti:yarn">
58 <Code code={`yarn add ${deps}\nyarn add -D ${devDeps}`} lang="sh" />
59 </TabItem>
60</Tabs>
61
62`@vitejs/plugin-rsc` is what splits the build into `client`, `ssr`, and
63`rsc` environments; `@vitejs/plugin-react` handles the usual JSX/HMR
64transform.
65
66## Configure the RSC plugin
67
68Point `rsc()` at one entry per environment:
69
70```typescript
71// vite.config.ts
72import react from "@vitejs/plugin-react";
73import rsc from "@vitejs/plugin-rsc";
74import { defineConfig } from "vite";
75
76export default defineConfig({
77 plugins: [
78 react(),
79 rsc({
80 serverHandler: false,
81 entries: {
82 client: "./react-router-vite/entry.browser.tsx",
83 ssr: "./react-router-vite/entry.ssr.tsx",
84 rsc: "./react-router-vite/entry.worker.tsx",
85 },
86 }),
87 ],
88 optimizeDeps: {
89 include: ["react-router", "react-router/internal/react-server-client"],
90 },
91});
92```
93
94`client` is the browser bundle, `ssr` renders HTML, and `rsc` is the
95Worker entry — a module that default-exports the `{ fetch }` handler
96Cloudflare invokes, which is also why `serverHandler` is off: the Worker
97serves requests itself. Alchemy injects its Cloudflare Vite plugin on
98top of this config at build and dev time, so there is nothing
99Cloudflare-specific to add here.
100
101## Declare the environment inputs
102
103Give each server environment its own build input:
104
105```diff lang="typescript"
106// vite.config.ts
107export default defineConfig({
108 plugins: [react(), rsc({ ... })],
109+ environments: {
110+ // The Worker is the RSC environment.
111+ rsc: {
112+ build: {
113+ rollupOptions: {
114+ input: { "entry.worker": "./react-router-vite/entry.worker.tsx" },
115+ },
116+ },
117+ },
118+ // A second ssr input the Worker loads on demand.
119+ ssr: {
120+ build: {
121+ rollupOptions: {
122+ input: { "worker-ssr": "./react-router-vite/worker-ssr.tsx" },
123+ },
124+ },
125+ },
126+ },
127});
128```
129
130The `rsc` input is the Worker entry, and the extra `ssr` input
131(`worker-ssr`) is a chunk the Worker loads across environments at
132runtime via `import.meta.viteRsc.loadModule("ssr", "worker-ssr")` — the
133pattern for Worker code that needs a non-`react-server` module such as
134`react-dom/server`, which must not be imported directly from the `rsc`
135entry.
136
137## Declare the Website
138
139Declare the site as a module-level const (rather than inline in the
140Stack) and describe the environment topology with `viteEnvironments`:
141
142```typescript
143// alchemy.run.ts
144import * as Cloudflare from "alchemy/Cloudflare";
145
146export const Website = Cloudflare.Website.Vite("Website", {
147 viteEnvironments: {
148 entry: "rsc",
149 children: ["ssr"],
150 },
151});
152
153export type WebsiteEnv = Cloudflare.InferEnv<typeof Website>;
154```
155
156No asset routing config is needed: the RSC build emits no
157`index.html` (HTML is server-rendered), so page requests match no
158static asset and fall through to the RSC handler, while the
159hydration bundles serve directly from the asset layer. The server
160bundle's Node APIs are covered by the `nodejs_compat` compatibility
161flag, which is enabled by default for every Worker.
162
163## Add it to the Stack
164
165Yield the Website from your Stack and return its URL:
166
167```typescript
168// alchemy.run.ts
169import * as Alchemy from "alchemy";
170import * as Effect from "effect/Effect";
171
172export default Alchemy.Stack(
173 "ReactRouterApp",
174 {
175 providers: Cloudflare.providers(),
176 state: Cloudflare.state(),
177 },
178 Effect.gen(function* () {
179 const website = yield* Website;
180 return { url: website.url };
181 }),
182);
183```
184
185## Add bindings
186
187The resource returns a plain `Worker`, so `env` accepts the full
188binding vocabulary — KV namespaces, R2 buckets, Durable Objects,
189secrets:
190
191```diff lang="typescript"
192// alchemy.run.ts
193+import * as Config from "effect/Config";
194
195+export const Uploads = Cloudflare.R2.Bucket("Uploads");
196
197export const Website = Cloudflare.Website.Vite("Website", {
198+ env: {
199+ UPLOADS: Uploads,
200+ API_KEY: Config.Redacted("API_KEY"),
201+ },
202 viteEnvironments: {
203 entry: "rsc",
204 children: ["ssr"],
205 },
206});
207```
208
209`Uploads` is a description, not a deploy — Alchemy provisions the
210real bucket because the Website binds it. `Config.Redacted` reads
211`API_KEY` from your environment at deploy time and binds it as a
212Worker secret — see [Secrets & env](/cloudflare/security/secrets-env).
213
214The Worker entry receives `env` as the standard second argument of
215the module-worker `fetch` contract — type it with the inferred
216shape:
217
218```typescript
219// react-router-vite/entry.worker.tsx
220import type { WebsiteEnv } from "../alchemy.run.ts";
221
222export default {
223 async fetch(request: Request, env: WebsiteEnv, ctx: ExecutionContext) {
224 // env.UPLOADS and env.API_KEY are fully typed here
225 // ... hand off to the RSC request handler
226 },
227};
228```
229
230## How viteEnvironments maps to the deploy
231
232`viteEnvironments` selects which of the build's environments make up
233the deployed Worker:
234
235```typescript
236viteEnvironments: {
237 entry: "rsc", // this environment's output is the Worker entry chunk
238 children: ["ssr"], // these environments' chunks are bundled alongside it
239}
240```
241
242The `entry` environment produces the chunk the Worker boots from, every
243environment listed in `children` has its chunks bundled alongside so
244cross-environment `loadModule` calls resolve inside the deployed Worker,
245and the `client` environment is always deployed as static assets — it is
246never listed. The default is `{ entry: "ssr", children: [] }`, so a
247React Router app that doesn't use Server Components — a
248single-environment SSR build — needs no `viteEnvironments` at all.
249
250## Deploy and verify
251
252```sh
253bun alchemy deploy
254```
255
256This exact shape is exercised by Alchemy's live test suite ("Vite:
257React Router RSC deploys from a distilled manifest"): the test deploys
258the app to real Cloudflare and asserts the home route serves
259server-rendered HTML, the `/about` client route resolves, and a
260`/worker-render` route returns HTML rendered through the `ssr`
261environment's chunk — server rendering and client navigation are both
262verified against the deployed Worker.