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
- Read the logged per-route errors immediately preceding the throw to find the offending `routeId` and export.
- Remove `headers`/`action` exports from routes when running `ssr:false`.
- For routes with `loader`: add a `clientLoader`, or include the route's path in `prerender`, or remove the `loader`.
- 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
- Under `ssr:false`, never export `action` or `headers`.
- Only export `loader` on routes covered by `prerender`, or pair it with a `clientLoader`.
- Audit parent layout loaders when switching to `ssr:false`.
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
- Prerender (data): Received a ${response.status} status code
- Prerender (resource): Received a ${response.status} status c
- SPA Mode: Received a ${response.status} status code from `en
- Prerender (html): Received a ${response.status} status code
- Prerender: Request failed for ${prerenderPath}: ${response.s
AI-assisted analysis of remix-run/react-router@1fd704a7da (2026-08-12).
Data as JSON: /api/errors/c4b04bdd9eb0c110.
Report an issue: GitHub.