Skip to content

Commit cd237e3

Browse files
committed
implement typesafe useRouteLoaderData
enable via typesafeUseRouteLoaderData future flag
1 parent c9487e6 commit cd237e3

File tree

7 files changed

+76
-4
lines changed

7 files changed

+76
-4
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@react-router/dev": minor
3+
"react-router": minor
4+
---
5+
6+
Add a new `typesafeUseRouteLoaderData` future flag, which, when enabled, improves the type of `useRouteLoaderData` with inference and errors.

contributors.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@
136136
- jenseng
137137
- JeraldVin
138138
- JesusTheHun
139+
- jeyj0
139140
- jimniels
140141
- jmargeta
141142
- johnpangalos

packages/react-router-dev/config/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ type ServerModuleFormat = "esm" | "cjs";
8585

8686
interface FutureConfig {
8787
unstable_optimizeDeps: boolean;
88+
typesafeUseRouteLoaderData: boolean;
8889
}
8990

9091
export type BuildManifest = DefaultBuildManifest | ServerBundlesBuildManifest;
@@ -482,6 +483,8 @@ async function resolveConfig({
482483
let future: FutureConfig = {
483484
unstable_optimizeDeps:
484485
reactRouterUserConfig.future?.unstable_optimizeDeps ?? false,
486+
typesafeUseRouteLoaderData:
487+
reactRouterUserConfig.future?.typesafeUseRouteLoaderData ?? false,
485488
};
486489

487490
let reactRouterConfig: ResolvedReactRouterConfig = deepFreeze({

packages/react-router-dev/typegen/generate.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as Pathe from "pathe/utils";
44

55
import { type RouteManifest, type RouteManifestEntry } from "../config/routes";
66
import { type Context } from "./context";
7-
import { getTypesPath } from "./paths";
7+
import { getTypesDir, getTypesPath } from "./paths";
88

99
export function generate(ctx: Context, route: RouteManifestEntry): string {
1010
const lineage = getRouteLineage(ctx.config.routes, route);
@@ -120,3 +120,44 @@ function parseParams(urlpath: string) {
120120
if (hasSplat) result["*"] = [false];
121121
return result;
122122
}
123+
124+
export function generateRouteManifest(ctx: Context) {
125+
return ts`
126+
export type RouteManifest = {
127+
${Object.entries(ctx.config.routes)
128+
.map(([routeId, routeEntry], i) => {
129+
const indent = i === 0 ? "" : " ".repeat(3);
130+
const routeTypeFile = Path.relative(
131+
getTypesDir(ctx),
132+
getTypesPath(ctx, routeEntry)
133+
);
134+
return `${indent}"${routeId}": import("./${routeTypeFile}").Info,`;
135+
})
136+
.join("\n")}
137+
};
138+
139+
export type RouteId = keyof RouteManifest;
140+
`;
141+
}
142+
143+
export function generateUseRouteLoaderDataType(ctx: Context) {
144+
if (ctx.config.future.typesafeUseRouteLoaderData) {
145+
return ts`
146+
import type { RouteId, RouteManifest } from "./routeManifest";
147+
148+
declare module "react-router" {
149+
export function useRouteLoaderData<RI extends RouteId>(
150+
id: RI,
151+
): RouteManifest[RI]["loaderData"];
152+
}
153+
`;
154+
}
155+
156+
return ts`
157+
declare module "react-router" {
158+
export function useRouteLoaderData<T = any>(
159+
routeId: string
160+
): SerializeFrom<T> | undefined;
161+
}
162+
`;
163+
}

packages/react-router-dev/typegen/index.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@ import type vite from "vite";
66

77
import { createConfigLoader } from "../config/config";
88

9-
import { generate } from "./generate";
9+
import {
10+
generate,
11+
generateRouteManifest,
12+
generateUseRouteLoaderDataType,
13+
} from "./generate";
1014
import type { Context } from "./context";
11-
import { getTypesDir, getTypesPath } from "./paths";
15+
import { getTypesDir, getTypesPath, getGlobalTypesFilePath } from "./paths";
1216

1317
export async function run(rootDirectory: string) {
1418
const ctx = await createContext({ rootDirectory, watch: false });
@@ -81,4 +85,17 @@ async function writeAll(ctx: Context): Promise<void> {
8185
fs.mkdirSync(Path.dirname(typesPath), { recursive: true });
8286
fs.writeFileSync(typesPath, content);
8387
});
88+
89+
const useRouteLoaderDataContent = generateUseRouteLoaderDataType(ctx);
90+
const useRouteLoaderDataTypesPath = getGlobalTypesFilePath(
91+
ctx,
92+
"useRouteLoaderData"
93+
);
94+
fs.writeFileSync(useRouteLoaderDataTypesPath, useRouteLoaderDataContent);
95+
96+
if (ctx.config.future.typesafeUseRouteLoaderData) {
97+
const routeManifestContent = generateRouteManifest(ctx);
98+
const routeManifestPath = getGlobalTypesFilePath(ctx, "routeManifest");
99+
fs.writeFileSync(routeManifestPath, routeManifestContent);
100+
}
84101
}

packages/react-router-dev/typegen/paths.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,7 @@ export function getTypesPath(ctx: Context, route: RouteManifestEntry) {
1616
"+types/" + Pathe.filename(route.file) + ".ts"
1717
);
1818
}
19+
20+
export function getGlobalTypesFilePath(ctx: Context, fileName: string) {
21+
return Path.join(getTypesDir(ctx), Pathe.filename(fileName) + ".d.ts");
22+
}

packages/react-router/lib/hooks.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1117,7 +1117,7 @@ export function useLoaderData<T = any>(): SerializeFrom<T> {
11171117
@category Hooks
11181118
*/
11191119
export function useRouteLoaderData<T = any>(
1120-
routeId: string
1120+
routeId: never // actually string after typegen; never allows typesafeUseRouteLoaderData future flag to work
11211121
): SerializeFrom<T> | undefined {
11221122
let state = useDataRouterState(DataRouterStateHook.UseRouteLoaderData);
11231123
return state.loaderData[routeId] as SerializeFrom<T> | undefined;

0 commit comments

Comments
 (0)