Skip to content

Commit f43b55e

Browse files
fix: move with-props logic to core library (#13650)
1 parent 203309c commit f43b55e

File tree

7 files changed

+110
-157
lines changed

7 files changed

+110
-157
lines changed

.changeset/calm-pants-film.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@react-router/dev": patch
3+
"react-router": patch
4+
---
5+
6+
Avoid additional `with-props` chunk in Framework Mode by moving route module component prop logic from the Vite plugin to `react-router`

integration/prefetch-test.ts

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,6 @@ test.describe("prefetch", () => {
165165

166166
// These 2 are common and duped for both - but they've already loaded on
167167
// page load so they don't trigger network requests
168-
await page.waitForSelector(
169-
"link[rel='modulepreload'][href^='/assets/with-props-']",
170-
{ state: "attached" }
171-
);
172168
await page.waitForSelector(
173169
// Look for either Rollup or Rolldown chunks
174170
[
@@ -179,7 +175,7 @@ test.describe("prefetch", () => {
179175
);
180176

181177
// Ensure no other links in the #nav element
182-
expect(await page.locator("link").count()).toBe(7);
178+
expect(await page.locator("link").count()).toBe(5);
183179
});
184180
});
185181

@@ -225,10 +221,6 @@ test.describe("prefetch", () => {
225221
"link[rel='modulepreload'][href^='/assets/prefetch-with-loader-']",
226222
{ state: "attached" }
227223
);
228-
await page.waitForSelector(
229-
"link[rel='modulepreload'][href^='/assets/with-props-']",
230-
{ state: "attached" }
231-
);
232224
await page.waitForSelector(
233225
// Look for either Rollup or Rolldown chunks
234226
[
@@ -237,17 +229,13 @@ test.describe("prefetch", () => {
237229
].join(","),
238230
{ state: "attached" }
239231
);
240-
expect(await page.locator("link").count()).toBe(4);
232+
expect(await page.locator("link").count()).toBe(3);
241233

242234
await page.hover("a[href='/prefetch-without-loader']");
243235
await page.waitForSelector(
244236
"link[rel='modulepreload'][href^='/assets/prefetch-without-loader-']",
245237
{ state: "attached" }
246238
);
247-
await page.waitForSelector(
248-
"link[rel='modulepreload'][href^='/assets/with-props-']",
249-
{ state: "attached" }
250-
);
251239
await page.waitForSelector(
252240
// Look for either Rollup or Rolldown chunks
253241
[
@@ -256,7 +244,7 @@ test.describe("prefetch", () => {
256244
].join(","),
257245
{ state: "attached" }
258246
);
259-
expect(await page.locator("link").count()).toBe(3);
247+
expect(await page.locator("link").count()).toBe(2);
260248
});
261249

262250
test("removes prefetch tags after navigating to/from the page", async ({
@@ -268,7 +256,7 @@ test.describe("prefetch", () => {
268256
// Links added on hover
269257
await page.hover("a[href='/prefetch-with-loader']");
270258
await page.waitForSelector("link", { state: "attached" });
271-
expect(await page.locator("link").count()).toBe(4);
259+
expect(await page.locator("link").count()).toBe(3);
272260

273261
// Links removed upon navigating to the page
274262
await page.click("a[href='/prefetch-with-loader']");
@@ -334,10 +322,6 @@ test.describe("prefetch", () => {
334322
"link[rel='modulepreload'][href^='/assets/prefetch-with-loader-']",
335323
{ state: "attached" }
336324
);
337-
await page.waitForSelector(
338-
"link[rel='modulepreload'][href^='/assets/with-props-']",
339-
{ state: "attached" }
340-
);
341325
await page.waitForSelector(
342326
// Look for either Rollup or Rolldown chunks
343327
[
@@ -346,17 +330,13 @@ test.describe("prefetch", () => {
346330
].join(","),
347331
{ state: "attached" }
348332
);
349-
expect(await page.locator("link").count()).toBe(4);
333+
expect(await page.locator("link").count()).toBe(3);
350334

351335
await page.focus("a[href='/prefetch-without-loader']");
352336
await page.waitForSelector(
353337
"link[rel='modulepreload'][href^='/assets/prefetch-without-loader-']",
354338
{ state: "attached" }
355339
);
356-
await page.waitForSelector(
357-
"link[rel='modulepreload'][href^='/assets/with-props-']",
358-
{ state: "attached" }
359-
);
360340
await page.waitForSelector(
361341
// Look for either Rollup or Rolldown chunks
362342
[
@@ -365,7 +345,7 @@ test.describe("prefetch", () => {
365345
].join(","),
366346
{ state: "attached" }
367347
);
368-
expect(await page.locator("link").count()).toBe(3);
348+
expect(await page.locator("link").count()).toBe(2);
369349
});
370350
});
371351

packages/react-router-dev/vite/plugin.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ import {
6969
resolveEntryFiles,
7070
configRouteToBranchRoute,
7171
} from "../config/config";
72-
import * as WithProps from "./with-props";
72+
import { decorateComponentExportsWithProps } from "./with-props";
7373

7474
export type LoadCssContents = (
7575
viteDevServer: Vite.ViteDevServer,
@@ -2149,7 +2149,6 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
21492149
}
21502150
},
21512151
},
2152-
WithProps.plugin,
21532152
{
21542153
name: "react-router:route-exports",
21552154
async transform(code, id, options) {
@@ -2201,7 +2200,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
22012200
if (!options?.ssr) {
22022201
removeExports(ast, SERVER_ONLY_ROUTE_EXPORTS);
22032202
}
2204-
WithProps.transform(ast);
2203+
decorateComponentExportsWithProps(ast);
22052204
return generate(ast, {
22062205
sourceMaps: true,
22072206
filename: id,

packages/react-router-dev/vite/with-props.ts

Lines changed: 19 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,22 @@
1-
import type { Plugin } from "vite";
2-
import dedent from "dedent";
3-
41
import type { Babel, NodePath, ParseResult } from "./babel";
52
import { traverse, t } from "./babel";
6-
import * as VirtualModule from "./virtual-module";
7-
8-
const vmod = VirtualModule.create("with-props");
9-
10-
const NAMED_COMPONENT_EXPORTS = ["HydrateFallback", "ErrorBoundary"];
11-
12-
export const plugin: Plugin = {
13-
name: "react-router-with-props",
14-
enforce: "pre",
15-
resolveId(id) {
16-
if (id === vmod.id) return vmod.resolvedId;
17-
},
18-
async load(id) {
19-
if (id !== vmod.resolvedId) return;
20-
21-
// Note: If you make changes to these implementations, please also update
22-
// the corresponding functions in packages/react-router/lib/dom/ssr/routes-test-stub.tsx
23-
return dedent`
24-
import { createElement as h } from "react";
25-
import { useActionData, useLoaderData, useMatches, useParams, useRouteError } from "react-router";
26-
27-
export function withComponentProps(Component) {
28-
return function Wrapped() {
29-
const props = {
30-
params: useParams(),
31-
loaderData: useLoaderData(),
32-
actionData: useActionData(),
33-
matches: useMatches(),
34-
};
35-
return h(Component, props);
36-
};
37-
}
383

39-
export function withHydrateFallbackProps(HydrateFallback) {
40-
return function Wrapped() {
41-
const props = {
42-
params: useParams(),
43-
loaderData: useLoaderData(),
44-
actionData: useActionData(),
45-
};
46-
return h(HydrateFallback, props);
47-
};
48-
}
4+
const namedComponentExports = ["HydrateFallback", "ErrorBoundary"] as const;
5+
type NamedComponentExport = (typeof namedComponentExports)[number];
6+
function isNamedComponentExport(name: string): name is NamedComponentExport {
7+
return namedComponentExports.includes(name as NamedComponentExport);
8+
}
499

50-
export function withErrorBoundaryProps(ErrorBoundary) {
51-
return function Wrapped() {
52-
const props = {
53-
params: useParams(),
54-
loaderData: useLoaderData(),
55-
actionData: useActionData(),
56-
error: useRouteError(),
57-
};
58-
return h(ErrorBoundary, props);
59-
};
60-
}
61-
`;
62-
},
63-
};
10+
type HocName =
11+
| "UNSAFE_withComponentProps"
12+
| "UNSAFE_withHydrateFallbackProps"
13+
| "UNSAFE_withErrorBoundaryProps";
6414

65-
export const transform = (ast: ParseResult<Babel.File>) => {
15+
export const decorateComponentExportsWithProps = (
16+
ast: ParseResult<Babel.File>
17+
) => {
6618
const hocs: Array<[string, Babel.Identifier]> = [];
67-
function getHocUid(path: NodePath, hocName: string) {
19+
function getHocUid(path: NodePath, hocName: HocName) {
6820
const uid = path.scope.generateUidIdentifier(hocName);
6921
hocs.push([hocName, uid]);
7022
return uid;
@@ -80,7 +32,7 @@ export const transform = (ast: ParseResult<Babel.File>) => {
8032
declaration.isFunctionDeclaration() ? toFunctionExpression(declaration.node) :
8133
undefined
8234
if (expr) {
83-
const uid = getHocUid(path, "withComponentProps");
35+
const uid = getHocUid(path, "UNSAFE_withComponentProps");
8436
declaration.replaceWith(t.callExpression(uid, [expr]));
8537
}
8638
return;
@@ -97,9 +49,8 @@ export const transform = (ast: ParseResult<Babel.File>) => {
9749
if (!expr) return;
9850
if (!id.isIdentifier()) return;
9951
const { name } = id.node;
100-
if (!NAMED_COMPONENT_EXPORTS.includes(name)) return;
101-
102-
const uid = getHocUid(path, `with${name}Props`);
52+
if (!isNamedComponentExport(name)) return;
53+
const uid = getHocUid(path, `UNSAFE_with${name}Props`);
10354
init.replaceWith(t.callExpression(uid, [expr]));
10455
});
10556
return;
@@ -109,9 +60,9 @@ export const transform = (ast: ParseResult<Babel.File>) => {
10960
const { id } = decl.node;
11061
if (!id) return;
11162
const { name } = id;
112-
if (!NAMED_COMPONENT_EXPORTS.includes(name)) return;
63+
if (!isNamedComponentExport(name)) return;
11364

114-
const uid = getHocUid(path, `with${name}Props`);
65+
const uid = getHocUid(path, `UNSAFE_with${name}Props`);
11566
decl.replaceWith(
11667
t.variableDeclaration("const", [
11768
t.variableDeclarator(
@@ -131,7 +82,7 @@ export const transform = (ast: ParseResult<Babel.File>) => {
13182
hocs.map(([name, identifier]) =>
13283
t.importSpecifier(identifier, t.identifier(name))
13384
),
134-
t.stringLiteral(vmod.id)
85+
t.stringLiteral("react-router")
13586
)
13687
);
13788
}

packages/react-router/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,9 @@ export {
320320
export {
321321
hydrationRouteProperties as UNSAFE_hydrationRouteProperties,
322322
mapRouteProperties as UNSAFE_mapRouteProperties,
323+
withComponentProps as UNSAFE_withComponentProps,
324+
withHydrateFallbackProps as UNSAFE_withHydrateFallbackProps,
325+
withErrorBoundaryProps as UNSAFE_withErrorBoundaryProps,
323326
} from "./lib/components";
324327

325328
/** @internal */

packages/react-router/lib/components.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,16 @@ import {
5353
} from "./context";
5454
import {
5555
_renderMatches,
56+
useActionData,
5657
useAsyncValue,
5758
useInRouterContext,
59+
useLoaderData,
5860
useLocation,
61+
useMatches,
5962
useNavigate,
6063
useOutlet,
64+
useParams,
65+
useRouteError,
6166
useRoutes,
6267
useRoutesImpl,
6368
} from "./hooks";
@@ -1229,3 +1234,58 @@ export function renderMatches(
12291234
): React.ReactElement | null {
12301235
return _renderMatches(matches);
12311236
}
1237+
1238+
export type RouteComponentType = React.ComponentType<{
1239+
params: ReturnType<typeof useParams>;
1240+
loaderData: ReturnType<typeof useLoaderData>;
1241+
actionData: ReturnType<typeof useActionData>;
1242+
matches: ReturnType<typeof useMatches>;
1243+
}>;
1244+
1245+
export function withComponentProps(Component: RouteComponentType) {
1246+
return function WithComponentProps() {
1247+
const props = {
1248+
params: useParams(),
1249+
loaderData: useLoaderData(),
1250+
actionData: useActionData(),
1251+
matches: useMatches(),
1252+
};
1253+
return React.createElement(Component, props);
1254+
};
1255+
}
1256+
1257+
export type HydrateFallbackType = React.ComponentType<{
1258+
params: ReturnType<typeof useParams>;
1259+
loaderData: ReturnType<typeof useLoaderData>;
1260+
actionData: ReturnType<typeof useActionData>;
1261+
}>;
1262+
1263+
export function withHydrateFallbackProps(HydrateFallback: HydrateFallbackType) {
1264+
return function WithHydrateFallbackProps() {
1265+
const props = {
1266+
params: useParams(),
1267+
loaderData: useLoaderData(),
1268+
actionData: useActionData(),
1269+
};
1270+
return React.createElement(HydrateFallback, props);
1271+
};
1272+
}
1273+
1274+
export type ErrorBoundaryType = React.ComponentType<{
1275+
params: ReturnType<typeof useParams>;
1276+
loaderData: ReturnType<typeof useLoaderData>;
1277+
actionData: ReturnType<typeof useActionData>;
1278+
error: ReturnType<typeof useRouteError>;
1279+
}>;
1280+
1281+
export function withErrorBoundaryProps(ErrorBoundary: ErrorBoundaryType) {
1282+
return function WithErrorBoundaryProps() {
1283+
const props = {
1284+
params: useParams(),
1285+
loaderData: useLoaderData(),
1286+
actionData: useActionData(),
1287+
error: useRouteError(),
1288+
};
1289+
return React.createElement(ErrorBoundary, props);
1290+
};
1291+
}

0 commit comments

Comments
 (0)