diff --git a/.changeset/lovely-ants-laugh.md b/.changeset/lovely-ants-laugh.md
new file mode 100644
index 0000000000..b298c64a17
--- /dev/null
+++ b/.changeset/lovely-ants-laugh.md
@@ -0,0 +1,21 @@
+---
+"@react-router/dev": patch
+---
+
+Fix framework props for reexported components
+
+Previously, when re-exporting the `default` component, `HydrateFallback`, or `ErrorBoundary` their corresponding framework props like `params`, `loaderData`, and `actionData` were not provided, causing errors at runtime.
+Now, React Router detects re-exports for framework components and provides their corresponding props.
+
+For example, both of these now work:
+
+```ts
+export { default } from "./other-module";
+```
+
+```ts
+function Component({ params, loaderData, actionData }) {
+ /* ... */
+}
+export { Component as default };
+```
diff --git a/integration/framework-props-test.ts b/integration/framework-props-test.ts
new file mode 100644
index 0000000000..13497cc8b2
--- /dev/null
+++ b/integration/framework-props-test.ts
@@ -0,0 +1,117 @@
+import type { Page } from "@playwright/test";
+import { expect } from "@playwright/test";
+import dedent from "dedent";
+
+import type { Files } from "./helpers/vite.js";
+import { test, viteConfig } from "./helpers/vite.js";
+
+const tsx = dedent;
+
+const files: Files = async ({ port }) => ({
+ "vite.config.ts": tsx`
+ import { reactRouter } from "@react-router/dev/vite";
+
+ export default {
+ ${await viteConfig.server({ port })}
+ plugins: [reactRouter()],
+ }
+ `,
+ "app/lib/components.tsx": tsx`
+ function Component({ loaderData }: any) {
+ return
{loaderData.title}
;
+ }
+ export const ComponentAlias = Component;
+ export default Component;
+
+ export function HydrateFallback() {
+ return Loading...
;
+ }
+ export const HydrateFallbackAlias = HydrateFallback;
+
+ export function ErrorBoundary() {
+ return Error
;
+ }
+ export const ErrorBoundaryAlias = ErrorBoundary;
+ `,
+ "app/routes.ts": tsx`
+ import { type RouteConfig, index, route } from "@react-router/dev/routes";
+
+ export default [
+ route("named-reexport-with-source", "routes/named-reexport-with-source.tsx"),
+ route("alias-reexport-with-source", "routes/alias-reexport-with-source.tsx"),
+ route("named-reexport-without-source", "routes/named-reexport-without-source.tsx"),
+ route("alias-reexport-without-source", "routes/alias-reexport-without-source.tsx"),
+ ] satisfies RouteConfig;
+ `,
+ "app/routes/named-reexport-with-source.tsx": tsx`
+ export const loader = () => ({ title: "named-reexport-with-source" })
+
+ export {
+ default,
+ HydrateFallback,
+ ErrorBoundary,
+ } from "../lib/components"
+ `,
+ "app/routes/alias-reexport-with-source.tsx": tsx`
+ export const loader = () => ({ title: "alias-reexport-with-source" })
+
+ export {
+ ComponentAlias as default,
+ HydrateFallbackAlias as HydrateFallback,
+ ErrorBoundaryAlias as ErrorBoundary,
+ } from "../lib/components"
+ `,
+ "app/routes/named-reexport-without-source.tsx": tsx`
+ import { ComponentAlias, HydrateFallbackAlias, ErrorBoundaryAlias } from "../lib/components"
+
+ export const loader = () => ({ title: "named-reexport-without-source" })
+
+ export default ComponentAlias
+ const HydrateFallback = HydrateFallbackAlias
+ const ErrorBoundary = ErrorBoundaryAlias
+
+ export {
+ // note: it would be invalid syntax to use 'default' keyword here,
+ // so instead we 'export default' separately
+ HydrateFallback,
+ ErrorBoundary,
+ }
+ `,
+ "app/routes/alias-reexport-without-source.tsx": tsx`
+ import { ComponentAlias, HydrateFallbackAlias, ErrorBoundaryAlias } from "../lib/components"
+
+ export const loader = () => ({ title: "alias-reexport-without-source" })
+
+ export {
+ ComponentAlias as default,
+ HydrateFallbackAlias as HydrateFallback,
+ ErrorBoundaryAlias as ErrorBoundary,
+ }
+ `,
+});
+
+test("dev", async ({ page, dev }) => {
+ let { port } = await dev(files);
+ await workflow({ page, port });
+});
+
+test("build", async ({ page, reactRouterServe }) => {
+ let { port } = await reactRouterServe(files);
+ await workflow({ page, port });
+});
+
+async function workflow({ page, port }: { page: Page; port: number }) {
+ const routes = [
+ "named-reexport-with-source",
+ "alias-reexport-with-source",
+ "named-reexport-without-source",
+ "alias-reexport-without-source",
+ ];
+ for (const route of routes) {
+ await page.goto(`http://localhost:${port}/${route}`, {
+ waitUntil: "networkidle",
+ });
+ await expect(page.locator("[data-title]")).toHaveText(route);
+ expect(page.errors).toEqual([]);
+ }
+}
diff --git a/packages/react-router-dev/vite/with-props.ts b/packages/react-router-dev/vite/with-props.ts
index 46fbd2f400..62545acb40 100644
--- a/packages/react-router-dev/vite/with-props.ts
+++ b/packages/react-router-dev/vite/with-props.ts
@@ -22,6 +22,73 @@ export const decorateComponentExportsWithProps = (
return uid;
}
+ /**
+ * Rewrite any re-exports for named components (`default Component`, `HydrateFallback`, `ErrorBoundary`)
+ * into `export const = ` form in preparation for adding props HOCs in the next traversal.
+ *
+ * Case 1: `export { name, ... }` or `export { value as name, ... }`
+ * -> Rename `name` to `uid` where `uid` is a new unique identifier
+ * -> Insert `export const name = uid`
+ *
+ * Case 2: `export { name1, value as name 2, ... } from "source"`
+ * -> Insert `import { name as uid }` where `uid` is a new unique identifier
+ * -> Insert `export const name = uid`
+ */
+ traverse(ast, {
+ ExportNamedDeclaration(path) {
+ if (path.node.declaration) return;
+ const { source } = path.node;
+
+ const exports: Array<{
+ specifier: NodePath;
+ local: Babel.Identifier;
+ uid: Babel.Identifier;
+ exported: Babel.Identifier;
+ }> = [];
+ for (const specifier of path.get("specifiers")) {
+ if (specifier.isExportSpecifier()) {
+ const { local, exported } = specifier.node;
+ const { name } = local;
+ if (!t.isIdentifier(exported)) continue;
+ const uid = path.scope.generateUidIdentifier(`_${name}`);
+ if (exported.name === "default" || isNamedComponentExport(name)) {
+ exports.push({ specifier, local, uid, exported });
+ }
+ }
+ }
+ if (exports.length === 0) return;
+
+ if (source != null) {
+ // `import { local as uid } from "source"`
+ path.insertAfter([
+ t.importDeclaration(
+ exports.map(({ local, uid }) => t.importSpecifier(uid, local)),
+ source,
+ ),
+ ]);
+ } else {
+ const scope = path.scope.getProgramParent();
+ exports.forEach(({ local, uid }) => scope.rename(local.name, uid.name));
+ }
+
+ // `export const exported = uid`
+ path.insertAfter(
+ exports.map(({ uid, exported }) => {
+ if (exported.name === "default") {
+ return t.exportDefaultDeclaration(uid);
+ }
+ return t.exportNamedDeclaration(
+ t.variableDeclaration("const", [
+ t.variableDeclarator(exported, uid),
+ ]),
+ );
+ }),
+ );
+
+ exports.forEach(({ specifier }) => specifier.remove());
+ },
+ });
+
traverse(ast, {
ExportDeclaration(path) {
if (path.isExportDefaultDeclaration()) {