remix-run/react-router · error
Invalid route exports found when prerendering with…
Error message
Invalid route exports found when prerendering with `ssr:false`
What it means
With `ssr: false`, there is no runtime server, so prerendering validates route module exports before building: `headers` and `action` are never valid (nothing can handle requests), and `loader` is only valid on routes actually matched by a `prerender` path (their data is baked in at build time). Detailed per-route messages are logged first (with a link to the pre-rendering docs), then this aggregate error stops the build.
Solutions
- Remove `action` and `headers` exports from routes when `ssr: false` (forms must POST to an external API or use `clientAction`)
- Convert `loader` to `clientLoader` on routes that are not prerendered
- Add the affected paths to `prerender` so their `loader` runs at build time, or remove the loader
- Read the logged per-route errors above the throw — they name the exact route IDs and invalid exports
Example fix
// before (app/routes/contact.tsx, ssr:false)
export async function action({ request }: Route.ActionArgs) {
await submitContact(await request.formData());
return null;
}
// after — client-side submission in SPA Mode
export async function clientAction({ request }: Route.ClientActionArgs) {
await submitContact(await request.formData());
return null;
} Defensive patterns
Strategy: validation
Validate before calling
// Scan route modules for server-only exports before enabling ssr:false
import glob from 'fast-glob';
const bad: string[] = [];
for (const file of await glob('app/routes/**/*.ts*')) {
const src = fs.readFileSync(file, 'utf8');
if (/^export\s+(async\s+)?(function|const)\s+(action|headers|loader)\b/m.test(src)) {
bad.push(`${file}: server export found`);
}
}
if (bad.length) throw new Error(bad.join('\n')); Type guard
type ServerExport = 'loader' | 'action' | 'headers'; const isServerOnlyExport = (name: string): name is ServerExport => name === 'loader' || name === 'action' || name === 'headers';
Try / catch
try {
await build();
} catch (e) {
if (e instanceof Error && e.message.includes('Invalid route exports')) {
// per-route details were logged just above the throw — parse them from build logs
}
} Prevention
- When flipping `ssr: false`, grep `app/routes` for `export action|export headers|export loader` and migrate them
- Prefer `clientLoader`/`clientAction` during SPA migration
- Prerender every path whose loader data must be static
When it happens
Trigger: `ssr: false` plus any route exporting `action` or `headers`; a route with `loader` that is not covered by any path in the `prerender` config; a parent route with `loader` (and no `clientLoader`) above a prerendered child; enabling `prerender: true` only over part of the tree while other branches keep loaders.
Common situations: Flipping an SSR app to SPA Mode without stripping server exports; keeping form `action`s in routes during a SPA migration; prerendering only `/` while deeper routes still have loaders; inherited layout loaders.
Related errors
- SPA Mode: Received a
- SPA Mode: Did you forget to include `<Scripts/>` in your…
- The `prerender.concurrency` config must be a positive…
- The `prerender`/`prerender.paths` config must be a boolean…
- Expected a route.id in react-router processRoutes() function
AI-assisted analysis of remix-run/react-router@6beaca3952 (2026-08-18).
Data as JSON: /api/errors/c4b04bdd9eb0c110.
Report an issue: GitHub.
Appendix: 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 6beaca3952)