Skip to content

Commit

Permalink
implement typesafe useRouteLoaderData
Browse files Browse the repository at this point in the history
enable via typesafeUseRouteLoaderData future flag
  • Loading branch information
jeyj0 committed Dec 13, 2024
1 parent c9487e6 commit cd237e3
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 4 deletions.
6 changes: 6 additions & 0 deletions .changeset/eleven-insects-tickle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@react-router/dev": minor
"react-router": minor
---

Add a new `typesafeUseRouteLoaderData` future flag, which, when enabled, improves the type of `useRouteLoaderData` with inference and errors.
1 change: 1 addition & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
- jenseng
- JeraldVin
- JesusTheHun
- jeyj0
- jimniels
- jmargeta
- johnpangalos
Expand Down
3 changes: 3 additions & 0 deletions packages/react-router-dev/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ type ServerModuleFormat = "esm" | "cjs";

interface FutureConfig {
unstable_optimizeDeps: boolean;
typesafeUseRouteLoaderData: boolean;
}

export type BuildManifest = DefaultBuildManifest | ServerBundlesBuildManifest;
Expand Down Expand Up @@ -482,6 +483,8 @@ async function resolveConfig({
let future: FutureConfig = {
unstable_optimizeDeps:
reactRouterUserConfig.future?.unstable_optimizeDeps ?? false,
typesafeUseRouteLoaderData:
reactRouterUserConfig.future?.typesafeUseRouteLoaderData ?? false,
};

let reactRouterConfig: ResolvedReactRouterConfig = deepFreeze({
Expand Down
43 changes: 42 additions & 1 deletion packages/react-router-dev/typegen/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as Pathe from "pathe/utils";

import { type RouteManifest, type RouteManifestEntry } from "../config/routes";
import { type Context } from "./context";
import { getTypesPath } from "./paths";
import { getTypesDir, getTypesPath } from "./paths";

export function generate(ctx: Context, route: RouteManifestEntry): string {
const lineage = getRouteLineage(ctx.config.routes, route);
Expand Down Expand Up @@ -120,3 +120,44 @@ function parseParams(urlpath: string) {
if (hasSplat) result["*"] = [false];
return result;
}

export function generateRouteManifest(ctx: Context) {
return ts`
export type RouteManifest = {
${Object.entries(ctx.config.routes)
.map(([routeId, routeEntry], i) => {
const indent = i === 0 ? "" : " ".repeat(3);
const routeTypeFile = Path.relative(
getTypesDir(ctx),
getTypesPath(ctx, routeEntry)
);
return `${indent}"${routeId}": import("./${routeTypeFile}").Info,`;
})
.join("\n")}
};
export type RouteId = keyof RouteManifest;
`;
}

export function generateUseRouteLoaderDataType(ctx: Context) {
if (ctx.config.future.typesafeUseRouteLoaderData) {
return ts`
import type { RouteId, RouteManifest } from "./routeManifest";
declare module "react-router" {
export function useRouteLoaderData<RI extends RouteId>(
id: RI,
): RouteManifest[RI]["loaderData"];
}
`;
}

return ts`
declare module "react-router" {
export function useRouteLoaderData<T = any>(
routeId: string
): SerializeFrom<T> | undefined;
}
`;
}
21 changes: 19 additions & 2 deletions packages/react-router-dev/typegen/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ import type vite from "vite";

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

import { generate } from "./generate";
import {
generate,
generateRouteManifest,
generateUseRouteLoaderDataType,
} from "./generate";
import type { Context } from "./context";
import { getTypesDir, getTypesPath } from "./paths";
import { getTypesDir, getTypesPath, getGlobalTypesFilePath } from "./paths";

export async function run(rootDirectory: string) {
const ctx = await createContext({ rootDirectory, watch: false });
Expand Down Expand Up @@ -81,4 +85,17 @@ async function writeAll(ctx: Context): Promise<void> {
fs.mkdirSync(Path.dirname(typesPath), { recursive: true });
fs.writeFileSync(typesPath, content);
});

const useRouteLoaderDataContent = generateUseRouteLoaderDataType(ctx);
const useRouteLoaderDataTypesPath = getGlobalTypesFilePath(
ctx,
"useRouteLoaderData"
);
fs.writeFileSync(useRouteLoaderDataTypesPath, useRouteLoaderDataContent);

if (ctx.config.future.typesafeUseRouteLoaderData) {
const routeManifestContent = generateRouteManifest(ctx);
const routeManifestPath = getGlobalTypesFilePath(ctx, "routeManifest");
fs.writeFileSync(routeManifestPath, routeManifestContent);
}
}
4 changes: 4 additions & 0 deletions packages/react-router-dev/typegen/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ export function getTypesPath(ctx: Context, route: RouteManifestEntry) {
"+types/" + Pathe.filename(route.file) + ".ts"
);
}

export function getGlobalTypesFilePath(ctx: Context, fileName: string) {
return Path.join(getTypesDir(ctx), Pathe.filename(fileName) + ".d.ts");
}
2 changes: 1 addition & 1 deletion packages/react-router/lib/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1117,7 +1117,7 @@ export function useLoaderData<T = any>(): SerializeFrom<T> {
@category Hooks
*/
export function useRouteLoaderData<T = any>(
routeId: string
routeId: never // actually string after typegen; never allows typesafeUseRouteLoaderData future flag to work
): SerializeFrom<T> | undefined {
let state = useDataRouterState(DataRouterStateHook.UseRouteLoaderData);
return state.loaderData[routeId] as SerializeFrom<T> | undefined;
Expand Down

0 comments on commit cd237e3

Please sign in to comment.