Skip to main content
Frameworks

React Router on Easel

Deploy full-stack React Router 7 and 8 applications to Easel with server rendering, streaming, loaders, actions, middleware, static prerendering, and SPA support.

Easel deploys React Router 7 and 8 applications in Framework Mode. Add one deployment preset, and Easel serves browser assets and prerendered pages from the CDN and runs server rendering, loaders, actions, and resource routes in Functions. For how the platform fits together, see How Easel works.

At a glance

Support levelProduction-ready
Supported versionsReact Router 7 and 8
ModeFramework Mode
RenderingStatic, SPA, SSR, streaming, and prerendering
Server runtimeNode.js
Required preset@vercel/react-router
Automatic detectionYes
Most recently tested withReact Router 7.18.x

Deploy a React Router application

Install the deployment preset:

npm install -D @vercel/react-router

Add it to react-router.config.ts:

react-router.config.ts
import type { Config } from "@react-router/dev/config";
import { vercelPreset } from "@vercel/react-router/vite";

export default {
  presets: [vercelPreset()],
} satisfies Config;

The preset makes the build write the server entry Easel deploys. Then create a project and connect the Git repository, or deploy with the CLI:

easel deploy

Easel detects the project and runs react-router build. You can override the install command, build command, root directory, and Node.js version in build configuration.

React Router 7 and 8

React Router 8 is a non-breaking upgrade from 7, but it requires Node.js 22.22.0 or newer, React 19.2.7 or newer, Vite 7 or newer, and an ESM project. It also turns the future.v8_* behaviors on by default. Existing React Router 7 applications deploy without upgrading. Set the project's Node.js version to one that meets React Router 8's baseline before you upgrade.

To upgrade, move to the latest React Router 7 release, enable the future.v8_* flags and fix what they surface, then upgrade the React Router packages together, remove the flags, and test in a preview deployment.

What Easel deploys

React Router outputEasel resource
Browser JavaScript and CSSCDN assets
Public filesCDN assets
Prerendered pagesCDN assets
Server-rendered routesEasel Functions
Loaders and actionsEasel Functions
Resource routesEasel Functions
Server middlewareApplication function
SPA fallbackCDN routing configuration

The preset exposes the route structure, so server bundles can be split by route instead of shipping the whole server application as one Function.

Supported features

FeatureSupportNotes
Framework ModeSupportedUses the React Router Vite plugin
React Router 8SupportedRequires the React Router 8 runtime baseline
React Router 7SupportedFuture flags may be required for newer behavior
Server-side renderingSupportedDynamic document requests run in Easel Functions
Client-side navigationSupportedData requests are handled by the application function
Loaders and actionsSupportedHandle data loading, mutations, and form submissions
Nested routesSupportedIncludes nested layouts and data loading
Route error boundariesSupportedFramework error responses are preserved
StreamingSupportedIncludes Suspense and deferred route data
Resource routesSupportedReturn JSON, files, feeds, and other non-UI responses
Server middlewareSupportedStable in v8. Requires a future flag in v7
Client middlewareSupportedRuns in the browser during client navigations
Static prerenderingSupportedGenerated pages deploy to the CDN
SPA modeSupportedDeploys a client-rendered application with route fallback
HTTP redirectsSupportedIncludes redirects from loaders, actions, and middleware
Response headersSupportedIncludes route headers exports
Cookie sessionsSupportedCookie and Set-Cookie headers are preserved
File uploadsSupportedSubject to function limits
OpenTelemetrySupportedInstrument server code with standard SDKs. See tracing
WebSocketsNot supportedFunction routes use HTTP request-response semantics

Loaders and actions

Server rendering is on by default in Framework Mode. A loader runs on the server and passes data to the component through generated route types:

app/routes/product.tsx
import type { Route } from "./+types/product";

export async function loader({ params }: Route.LoaderArgs) {
  const product = await getProduct(params.productId);

  if (!product) {
    throw new Response("Not found", { status: 404 });
  }

  return { product };
}

export default function Product({ loaderData }: Route.ComponentProps) {
  return <h1>{loaderData.product.name}</h1>;
}

A document request invokes the application Function and streams the rendered response. After hydration, client navigations fetch loader data from the same Function without rerendering the document. Actions handle form submissions the same way, and redirects, cookies, and error boundaries follow the framework's normal response flow.

Streaming

A loader can return unresolved promises, and React Router streams the document before they settle:

app/routes/dashboard.tsx
import { Await } from "react-router";
import { Suspense } from "react";
import type { Route } from "./+types/dashboard";

export async function loader() {
  return {
    account: await getAccount(),
    activity: getRecentActivity(),
  };
}

export default function Dashboard({ loaderData }: Route.ComponentProps) {
  return (
    <Suspense fallback={<p>Loading activity…</p>}>
      <Await resolve={loaderData.activity}>
        {(activity) => <ActivityList activity={activity} />}
      </Await>
    </Suspense>
  );
}

Easel forwards chunks as React Router produces them. The Function stays active until the stream closes.

Middleware

Server middleware runs inside the application Function before and after the matched route's handlers. Use it for authentication, request context, and response headers. It's separate from the Firewall, which runs before a Function is invoked.

app/routes/dashboard.tsx
import { createContext, redirect } from "react-router";
import type { Route } from "./+types/dashboard";

export const userContext = createContext<User>();

const requireUser: Route.MiddlewareFunction = async (
  { request, context },
  next,
) => {
  const user = await authenticateRequest(request);

  if (!user) {
    throw redirect("/login");
  }

  context.set(userContext, user);
  return next();
};

export const middleware: Route.MiddlewareFunction[] = [requireUser];

export async function loader({ context }: Route.LoaderArgs) {
  return { user: context.get(userContext) };
}

Pass per-request data through context, not module-level variables. One Function instance handles many requests.

Middleware is on by default in React Router 8. React Router 7 releases that support it need the future flag:

react-router.config.ts
export default {
  presets: [vercelPreset()],
  future: {
    v8_middleware: true,
  },
} satisfies Config;

Response caching

Routes set CDN behavior with standard Cache-Control headers from a headers export:

app/routes/products.tsx
export function headers() {
  return {
    "Cache-Control": "public, s-maxage=60, stale-while-revalidate=300",
  };
}

Use private, no-store for responses that depend on the user or session. See CDN caching for how Easel caches and keys responses.

Static prerendering

List routes to generate during the build:

react-router.config.ts
export default {
  presets: [vercelPreset()],
  async prerender() {
    return ["/", "/about", "/pricing"];
  },
} satisfies Config;

Easel deploys the generated HTML to the CDN. Requests for prerendered routes don't invoke a Function.

SPA mode

Set ssr: false to deploy a client-rendered application:

react-router.config.ts
export default {
  ssr: false,
  presets: [vercelPreset()],
} satisfies Config;

Easel serves the build from the CDN and routes application paths to the SPA entry document. Server loaders and actions aren't deployed. clientLoader and clientAction still work. If the application uses React Router only in Data Mode or Declarative Mode, follow the Vite guide instead.

Sessions and environment variables

Cookie sessions from createCookieSessionStorage work through standard Set-Cookie headers and need no server storage. Use a database for session state that's too large for a cookie or must be revoked centrally.

Server code reads variables from process.env. Don't pass process.env to the browser. Return an explicit allowlist of public values from the root loader instead. See environment variables for scopes and how changes apply.

Project configuration

SettingDefault
Install commandDetected from the package manager
Build commandPackage script or react-router build
Development commandPackage script or react-router dev
Static assetsDetected from the framework build
Server outputDetected from the deployment preset
Node.js versionProject or platform default

In a monorepo, set the root directory to the folder containing the application's package.json, react-router.config.ts, Vite config, and routes. See monorepos.

Keep using react-router dev locally, and use a preview deployment to check the production server bundle, caching, and streaming.

Known limitations

  • WebSockets. Functions can't hold long-lived WebSocket connections. See Functions runtime.
  • Custom Node.js servers. Easel deploys the Web API request handler the preset generates. An application that needs to own a persistent HTTP server, raw TCP sockets, or process-level connection handling needs a separate service.

Troubleshooting

The build fails after upgrading to React Router 8

Check the Node.js, React, and Vite versions against React Router 8's baseline, and confirm the project is ESM. Then remove obsolete future.v8_* flags and check that @vercel/react-router and your Vite plugins support v8.

Middleware doesn't run in React Router 7

Check that the installed release supports middleware and that future.v8_middleware is on. A client navigation to a route with no server loader or action doesn't reach the server, so server middleware doesn't run.

A loader runs locally but not in production

Check that ssr: false isn't set. SPA mode doesn't deploy server loaders or actions.

Build with React Router's SPA mode and the preset, not as a plain Vite static build. The framework output includes the fallback route Easel needs.

Compatibility policy

Easel tests this guide against the version in At a glance. Test new React Router releases in a preview deployment first. See the compatibility policy.

Next steps