diff --git a/.changeset/route-component-export-props.md b/.changeset/route-component-export-props.md new file mode 100644 index 0000000000..8ed1ce9de5 --- /dev/null +++ b/.changeset/route-component-export-props.md @@ -0,0 +1,13 @@ +--- +"@react-router/dev": minor +"react-router": minor +--- + +Params, loader data, and action data as props for route component exports + +```tsx +export default function Component({ params, loaderData, actionData }) {} + +export function HydrateFallback({ params }) {} +export function ErrorBoundary({ params, loaderData, actionData }) {} +``` diff --git a/packages/react-router-dev/package.json b/packages/react-router-dev/package.json index 1766841548..20fa741ba6 100644 --- a/packages/react-router-dev/package.json +++ b/packages/react-router-dev/package.json @@ -53,6 +53,7 @@ "arg": "^5.0.1", "babel-dead-code-elimination": "^1.0.6", "chalk": "^4.1.2", + "dedent": "^1.5.3", "es-module-lexer": "^1.3.1", "exit-hook": "2.2.1", "fs-extra": "^10.0.0", diff --git a/packages/react-router-dev/vite/plugin.ts b/packages/react-router-dev/vite/plugin.ts index 3b6f7a4211..bf7f84eee3 100644 --- a/packages/react-router-dev/vite/plugin.ts +++ b/packages/react-router-dev/vite/plugin.ts @@ -45,7 +45,7 @@ import { resolveEntryFiles, resolvePublicPath, } from "./config"; -import { withProps } from "./with-props"; +import * as WithProps from "./with-props"; export async function resolveViteConfig({ configFile, @@ -1414,6 +1414,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = (_config) => { } }, }, + WithProps.plugin, { name: "react-router-route-exports", async transform(code, id, options) { @@ -1454,7 +1455,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = (_config) => { let ast = parse(code, { sourceType: "module" }); removeExports(ast, SERVER_ONLY_ROUTE_EXPORTS); - withProps(ast); + WithProps.transform(ast); return generate(ast, { sourceMaps: true, filename: id, diff --git a/packages/react-router-dev/vite/with-props.ts b/packages/react-router-dev/vite/with-props.ts index 5825dcac87..47ae4d6a5c 100644 --- a/packages/react-router-dev/vite/with-props.ts +++ b/packages/react-router-dev/vite/with-props.ts @@ -1,9 +1,61 @@ +import type { Plugin } from "vite"; +import dedent from "dedent"; + import type { Babel, NodePath, ParseResult } from "./babel"; import { traverse, t } from "./babel"; +import * as VirtualModule from "./vmod"; + +const vmodId = VirtualModule.id("with-props"); const NAMED_COMPONENT_EXPORTS = ["HydrateFallback", "ErrorBoundary"]; -export const withProps = (ast: ParseResult) => { +export const plugin: Plugin = { + name: "react-router-with-props", + enforce: "pre", + resolveId(id) { + if (id === vmodId) return VirtualModule.resolve(vmodId); + }, + async load(id) { + if (id !== VirtualModule.resolve(vmodId)) return; + return dedent` + import { createElement as h } from "react"; + import { useActionData, useLoaderData, useParams } from "react-router"; + + export function withComponentProps(Component) { + return function Wrapped() { + const props = { + params: useParams(), + loaderData: useLoaderData(), + actionData: useActionData(), + }; + return h(Component, props); + }; + } + + export function withHydrateFallbackProps(HydrateFallback) { + return function Wrapped() { + const props = { + params: useParams(), + }; + return h(HydrateFallback, props); + }; + } + + export function withErrorBoundaryProps(ErrorBoundary) { + return function Wrapped() { + const props = { + params: useParams(), + loaderData: useLoaderData(), + actionData: useActionData(), + }; + return h(ErrorBoundary, props); + }; + } + `; + }, +}; + +export const transform = (ast: ParseResult) => { const hocs: Array<[string, Babel.Identifier]> = []; function getHocUid(path: NodePath, hocName: string) { const uid = path.scope.generateUidIdentifier(hocName); @@ -72,7 +124,7 @@ export const withProps = (ast: ParseResult) => { hocs.map(([name, identifier]) => t.importSpecifier(identifier, t.identifier(name)) ), - t.stringLiteral("react-router") + t.stringLiteral(vmodId) ) ); } diff --git a/packages/react-router/index.ts b/packages/react-router/index.ts index b66baec327..0666543335 100644 --- a/packages/react-router/index.ts +++ b/packages/react-router/index.ts @@ -133,11 +133,6 @@ export { useRouteLoaderData, useRoutes, } from "./lib/hooks"; -export { - withComponentProps, - withHydrateFallbackProps, - withErrorBoundaryProps, -} from "./lib/hocs"; // Expose old RR DOM API export type { diff --git a/packages/react-router/lib/hocs.tsx b/packages/react-router/lib/hocs.tsx deleted file mode 100644 index 582752cd7a..0000000000 --- a/packages/react-router/lib/hocs.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import * as React from "react"; -import { useActionData, useLoaderData, useParams } from "./hooks"; - -export function withComponentProps( - Component: React.ComponentType<{ - params: unknown; - loaderData: unknown; - actionData: unknown; - }> -) { - return function Wrapped() { - const props = { - params: useParams(), - loaderData: useLoaderData(), - actionData: useActionData(), - }; - return ; - }; -} - -export function withHydrateFallbackProps( - HydrateFallback: React.ComponentType<{ params: unknown }> -) { - return function Wrapped() { - const props = { - params: useParams(), - }; - return ; - }; -} - -export function withErrorBoundaryProps( - ErrorBoundary: React.ComponentType<{ - params: unknown; - loaderData: unknown; - actionData: unknown; - }> -) { - return function Wrapped() { - const props = { - params: useParams(), - loaderData: useLoaderData(), - actionData: useActionData(), - }; - return ; - }; -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 657ab500bb..53b74a3888 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -657,6 +657,9 @@ importers: chalk: specifier: ^4.1.2 version: 4.1.2 + dedent: + specifier: ^1.5.3 + version: 1.5.3 es-module-lexer: specifier: ^1.3.1 version: 1.5.0 @@ -7531,6 +7534,15 @@ packages: optional: true dev: false + /dedent@1.5.3: + resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + dev: false + /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}