From e6046acfc0a48d89e99346d5d5c44b4c1ade2db4 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Thu, 5 Jun 2025 11:21:30 -0400 Subject: [PATCH 1/3] test --- integration/typegen-test.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/integration/typegen-test.ts b/integration/typegen-test.ts index c96e7e1ca0..1c3630821f 100644 --- a/integration/typegen-test.ts +++ b/integration/typegen-test.ts @@ -325,6 +325,36 @@ test.describe("typegen", () => { expect(proc.status).toBe(0); }); + test("clientLoader data should not be serialized", async () => { + const cwd = await createProject({ + "vite.config.ts": viteConfig, + "app/expect-type.ts": expectType, + "app/routes/_index.tsx": tsx` + import { useRouteLoaderData } from "react-router" + + import type { Expect, Equal } from "../expect-type" + import type { Route } from "./+types/_index" + + export function clientLoader({}: Route.ClientLoaderArgs) { + return { fn: () => 0 } + } + + export default function Component({ loaderData }: Route.ComponentProps) { + type Test1 = Expect number }>> + + const routeLoaderData = useRouteLoaderData("routes/_index") + type Test2 = Expect number} | undefined>> + + return

Hello, world!

+ } + `, + }); + const proc = typecheck(cwd); + expect(proc.stdout.toString()).toBe(""); + expect(proc.stderr.toString()).toBe(""); + expect(proc.status).toBe(0); + }); + test("custom app dir", async () => { const cwd = await createProject({ "vite.config.ts": viteConfig, From 0aa16679ddfc61f81e78f01b1442c02a10c0d25d Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Thu, 5 Jun 2025 11:35:24 -0400 Subject: [PATCH 2/3] do not serialize types for `useRouteLoaderData` --- .changeset/weak-turkeys-pretend.md | 20 ++++ packages/react-router/lib/types/route-data.ts | 82 +++++++++++++- .../lib/types/route-module-annotations.ts | 102 ++++-------------- 3 files changed, 121 insertions(+), 83 deletions(-) create mode 100644 .changeset/weak-turkeys-pretend.md diff --git a/.changeset/weak-turkeys-pretend.md b/.changeset/weak-turkeys-pretend.md new file mode 100644 index 0000000000..e55adddc7e --- /dev/null +++ b/.changeset/weak-turkeys-pretend.md @@ -0,0 +1,20 @@ +--- +"react-router": patch +--- + +Do not serialize types for `useRouteLoaderData` + +For types to distinguish a `clientLoader` from a `serverLoader`, you MUST annotate `clientLoader` args: + +```ts +// 👇 annotation required to skip serializing types +export function clientLoader({}: Route.ClientLoaderArgs) { + return { fn: () => "earth" }; +} + +function SomeComponent() { + const data = useRouteLoaderData("routes/this-route"); + const planet = data?.fn() ?? "world"; + return

Hello, {planet}!

; +} +``` diff --git a/packages/react-router/lib/types/route-data.ts b/packages/react-router/lib/types/route-data.ts index c60e376939..e4b7450a7c 100644 --- a/packages/react-router/lib/types/route-data.ts +++ b/packages/react-router/lib/types/route-data.ts @@ -2,8 +2,14 @@ import type { ClientLoaderFunctionArgs, ClientActionFunctionArgs, } from "../dom/ssr/routeModules"; -import type { DataWithResponseInit } from "../router/utils"; +import type { + DataWithResponseInit, + unstable_RouterContextProvider, +} from "../router/utils"; import type { Serializable } from "../server-runtime/single-fetch"; +import type { AppLoadContext } from "../server-runtime/data"; + +import type { MiddlewareEnabled } from "./future"; import type { RouteModule } from "./route-module"; import type { unstable_SerializesTo } from "./serializes-to"; import type { Equal, Expect, Func, IsAny, Pretty } from "./utils"; @@ -62,8 +68,80 @@ type ServerData = export type ServerDataFrom = ServerData>; export type ClientDataFrom = ClientData>; +export type ClientDataFunctionArgs = { + /** + * A {@link https://developer.mozilla.org/en-US/docs/Web/API/Request Fetch Request instance} which you can use to read the URL, the method, the "content-type" header, and the request body from the request. + * + * @note Because client data functions are called before a network request is made, the Request object does not include the headers which the browser automatically adds. React Router infers the "content-type" header from the enc-type of the form that performed the submission. + **/ + request: Request; + /** + * {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route. + * @example + * // app/routes.ts + * route("teams/:teamId", "./team.tsx"), + * + * // app/team.tsx + * export function clientLoader({ + * params, + * }: Route.ClientLoaderArgs) { + * params.teamId; + * // ^ string + * } + **/ + params: Params; + /** + * When `future.unstable_middleware` is not enabled, this is undefined. + * + * When `future.unstable_middleware` is enabled, this is an instance of + * `unstable_RouterContextProvider` and can be used to access context values + * from your route middlewares. You may pass in initial context values in your + * `` prop + */ + context: unstable_RouterContextProvider; +}; + +export type ServerDataFunctionArgs = { + /** A {@link https://developer.mozilla.org/en-US/docs/Web/API/Request Fetch Request instance} which you can use to read the url, method, headers (such as cookies), and request body from the request. */ + request: Request; + /** + * {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route. + * @example + * // app/routes.ts + * route("teams/:teamId", "./team.tsx"), + * + * // app/team.tsx + * export function loader({ + * params, + * }: Route.LoaderArgs) { + * params.teamId; + * // ^ string + * } + **/ + params: Params; + /** + * Without `future.unstable_middleware` enabled, this is the context passed in + * to your server adapter's `getLoadContext` function. It's a way to bridge the + * gap between the adapter's request/response API with your React Router app. + * It is only applicable if you are using a custom server adapter. + * + * With `future.unstable_middleware` enabled, this is an instance of + * `unstable_RouterContextProvider` and can be used for type-safe access to + * context value set in your route middlewares. If you are using a custom + * server adapter, you may provide an initial set of context values from your + * `getLoadContext` function. + */ + context: MiddlewareEnabled extends true + ? unstable_RouterContextProvider + : AppLoadContext; +}; + export type SerializeFrom = T extends (...args: infer Args) => unknown - ? Args extends [ClientLoaderFunctionArgs | ClientActionFunctionArgs] + ? Args extends [ + | ClientLoaderFunctionArgs + | ClientActionFunctionArgs + | ClientDataFunctionArgs + ] ? ClientDataFrom : ServerDataFrom : T; diff --git a/packages/react-router/lib/types/route-module-annotations.ts b/packages/react-router/lib/types/route-module-annotations.ts index 191ff35d16..dcc1e5314e 100644 --- a/packages/react-router/lib/types/route-module-annotations.ts +++ b/packages/react-router/lib/types/route-module-annotations.ts @@ -1,14 +1,14 @@ import type { MetaDescriptor } from "../dom/ssr/routeModules"; import type { Location } from "../router/history"; import type { LinkDescriptor } from "../router/links"; -import type { - unstable_MiddlewareNextFunction, - unstable_RouterContextProvider, -} from "../router/utils"; -import type { AppLoadContext } from "../server-runtime/data"; -import type { MiddlewareEnabled } from "./future"; +import type { unstable_MiddlewareNextFunction } from "../router/utils"; -import type { GetLoaderData, ServerDataFrom } from "./route-data"; +import type { + ClientDataFunctionArgs, + GetLoaderData, + ServerDataFrom, + ServerDataFunctionArgs, +} from "./route-data"; import type { RouteModule } from "./route-module"; import type { Pretty } from "./utils"; @@ -67,94 +67,34 @@ type HeadersArgs = { errorHeaders: Headers | undefined; }; -type ClientDataFunctionArgs = { - /** - * A {@link https://developer.mozilla.org/en-US/docs/Web/API/Request Fetch Request instance} which you can use to read the URL, the method, the "content-type" header, and the request body from the request. - * - * @note Because client data functions are called before a network request is made, the Request object does not include the headers which the browser automatically adds. React Router infers the "content-type" header from the enc-type of the form that performed the submission. - **/ - request: Request; - /** - * {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route. - * @example - * // app/routes.ts - * route("teams/:teamId", "./team.tsx"), - * - * // app/team.tsx - * export function clientLoader({ - * params, - * }: Route.ClientLoaderArgs) { - * params.teamId; - * // ^ string - * } - **/ - params: T["params"]; - /** - * When `future.unstable_middleware` is not enabled, this is undefined. - * - * When `future.unstable_middleware` is enabled, this is an instance of - * `unstable_RouterContextProvider` and can be used to access context values - * from your route middlewares. You may pass in initial context values in your - * `` prop - */ - context: unstable_RouterContextProvider; -}; - -type ServerDataFunctionArgs = { - /** A {@link https://developer.mozilla.org/en-US/docs/Web/API/Request Fetch Request instance} which you can use to read the url, method, headers (such as cookies), and request body from the request. */ - request: Request; - /** - * {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route. - * @example - * // app/routes.ts - * route("teams/:teamId", "./team.tsx"), - * - * // app/team.tsx - * export function loader({ - * params, - * }: Route.LoaderArgs) { - * params.teamId; - * // ^ string - * } - **/ - params: T["params"]; - /** - * Without `future.unstable_middleware` enabled, this is the context passed in - * to your server adapter's `getLoadContext` function. It's a way to bridge the - * gap between the adapter's request/response API with your React Router app. - * It is only applicable if you are using a custom server adapter. - * - * With `future.unstable_middleware` enabled, this is an instance of - * `unstable_RouterContextProvider` and can be used for type-safe access to - * context value set in your route middlewares. If you are using a custom - * server adapter, you may provide an initial set of context values from your - * `getLoadContext` function. - */ - context: MiddlewareEnabled extends true - ? unstable_RouterContextProvider - : AppLoadContext; -}; - type CreateServerMiddlewareFunction = ( - args: ServerDataFunctionArgs, + args: ServerDataFunctionArgs, next: unstable_MiddlewareNextFunction ) => MaybePromise; type CreateClientMiddlewareFunction = ( - args: ClientDataFunctionArgs, + args: ClientDataFunctionArgs, next: unstable_MiddlewareNextFunction ) => MaybePromise; -type CreateServerLoaderArgs = ServerDataFunctionArgs; +type CreateServerLoaderArgs = ServerDataFunctionArgs< + T["params"] +>; -type CreateClientLoaderArgs = ClientDataFunctionArgs & { +type CreateClientLoaderArgs = ClientDataFunctionArgs< + T["params"] +> & { /** This is an asynchronous function to get the data from the server loader for this route. On client-side navigations, this will make a {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API fetch} call to the React Router server loader. If you opt-into running your clientLoader on hydration, then this function will return the data that was already loaded on the server (via Promise.resolve). */ serverLoader: () => Promise>; }; -type CreateServerActionArgs = ServerDataFunctionArgs; +type CreateServerActionArgs = ServerDataFunctionArgs< + T["params"] +>; -type CreateClientActionArgs = ClientDataFunctionArgs & { +type CreateClientActionArgs = ClientDataFunctionArgs< + T["params"] +> & { /** This is an asynchronous function that makes the {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API fetch} call to the React Router server action for this route. */ serverAction: () => Promise>; }; From 3d7a44fcdadf73a31eee0f9f8470fb846cda742f Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Thu, 5 Jun 2025 12:41:27 -0400 Subject: [PATCH 3/3] Update .changeset/weak-turkeys-pretend.md Co-authored-by: Steven Liao --- .changeset/weak-turkeys-pretend.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/weak-turkeys-pretend.md b/.changeset/weak-turkeys-pretend.md index e55adddc7e..cf87aa1821 100644 --- a/.changeset/weak-turkeys-pretend.md +++ b/.changeset/weak-turkeys-pretend.md @@ -2,7 +2,7 @@ "react-router": patch --- -Do not serialize types for `useRouteLoaderData` +Do not serialize types for `useRouteLoaderData` For types to distinguish a `clientLoader` from a `serverLoader`, you MUST annotate `clientLoader` args: