remix-run/react-router · error · Error

Invalid route exports found when prerendering with `ssr:fals

Error message

Invalid route exports found when prerendering with `ssr:false`

What it means

Terminal throw after `validateSsrFalseExports` collects per-route error strings. With `ssr:false`, the only SSR-capable exports allowed are those on routes that are actually prerendered; `headers`/`action` are never allowed (no server at runtime), and `loader` is allowed only on routes matched by a `prerender` path (or with a `clientLoader`). The per-route errors are logged first, then this aggregate throw halts the build.

Source

Thrown at packages/react-router-dev/vite/plugin.ts:3168

      while (parentRoute && parentRoute.id !== "root") {
        if (parentRoute.hasLoader && !parentRoute.hasClientLoader) {
          errors.push(
            `Prerender: 1 invalid route export in \`${parentRoute.id}\` when ` +
              "pre-rendering with `ssr:false`: `loader`. " +
              "See https://reactrouter.com/how-to/pre-rendering#invalid-exports for more information.",
          );
        }
        parentRoute =
          parentRoute.parentId && parentRoute.parentId !== "root"
            ? manifest.routes[parentRoute.parentId]
            : null;
      }
    }
  }

  if (errors.length > 0) {
    viteConfig.logger.error(colors.red(errors.join("\n")));
    throw new Error(
      "Invalid route exports found when prerendering with `ssr:false`",
    );
  }
}

function getAddressableRoutes(routes: RouteManifest): RouteManifestEntry[] {
  let nonAddressableIds = new Set<string>();

  for (let id in routes) {
    let route = routes[id];

    // We omit the parent route of index routes since the index route takes ownership of its parent's path
    if (route.index) {
      invariant(
        route.parentId,
        `Expected index route "${route.id}" to have "parentId" set`,
      );
      nonAddressableIds.add(route.parentId);

View on GitHub (pinned to 1fd704a7da)

Solutions

  1. Read the logged per-route errors immediately preceding the throw to find the offending `routeId` and export.
  2. Remove `headers`/`action` exports from routes when running `ssr:false`.
  3. For routes with `loader`: add a `clientLoader`, or include the route's path in `prerender`, or remove the `loader`.
  4. For parent loaders failing: ensure the branch is prerendered or convert to `clientLoader`.

Example fix

// before: route has action under ssr:false
export const action = async () => { ... };
// after: drop server-only action, use clientLoader/FORM
export const clientAction = async () => { ... };
Defensive patterns

Strategy: validation

Validate before calling

// Pre-build check: scan routes for forbidden ssr:false exports
import { parse } from "es-module-lexer";
const FORBIDDEN = ["action", "headers"];
for (const [id, file] of Object.entries(routes)) {
  const [imports, exports] = parse(readFileSync(file, "utf8"));
  const bad = exports.filter(e => FORBIDDEN.includes(e));
  if (bad.length) console.warn(`${id} exports ${bad.join(",")} under ssr:false`);
}

Prevention

When it happens

Trigger: Setting `ssr:false` while a route exports `action` (no runtime server exists to call it); a route exports `loader` but is not covered by any `prerender` path and has no `clientLoader`; a parent layout route exports `loader` without `clientLoader` and is not on a prerendered branch.

Common situations: Migrating an SSR app to SPA/prerender mode without auditing exports; adding a new route with `action` to an `ssr:false` project; using a parent loader but only prerendering a leaf that doesn't pull it in.

Related errors


AI-assisted analysis of remix-run/react-router@1fd704a7da (2026-08-12). Data as JSON: /api/errors/c4b04bdd9eb0c110. Report an issue: GitHub.