Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .changeset/weak-turkeys-pretend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"react-router": patch
---

Do not serialize types for `useRouteLoaderData<typeof clientLoader()>`

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<typeof clientLoader>("routes/this-route");
const planet = data?.fn() ?? "world";
return <h1>Hello, {planet}!</h1>;
}
```
30 changes: 30 additions & 0 deletions integration/typegen-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Equal<typeof loaderData, { fn: () => number }>>

const routeLoaderData = useRouteLoaderData<typeof clientLoader>("routes/_index")
type Test2 = Expect<Equal<typeof routeLoaderData, { fn: () => number} | undefined>>

return <h1>Hello, world!</h1>
}
`,
});
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,
Expand Down
82 changes: 80 additions & 2 deletions packages/react-router/lib/types/route-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -62,8 +68,80 @@ type ServerData<T> =
export type ServerDataFrom<T> = ServerData<DataFrom<T>>;
export type ClientDataFrom<T> = ClientData<DataFrom<T>>;

export type ClientDataFunctionArgs<Params> = {
/**
* 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
* `<HydratedRouter unstable_getContext>` prop
*/
context: unstable_RouterContextProvider;
};

export type ServerDataFunctionArgs<Params> = {
/** 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> = T extends (...args: infer Args) => unknown
? Args extends [ClientLoaderFunctionArgs | ClientActionFunctionArgs]
? Args extends [
| ClientLoaderFunctionArgs
| ClientActionFunctionArgs
| ClientDataFunctionArgs<unknown>
]
? ClientDataFrom<T>
: ServerDataFrom<T>
: T;
Expand Down
102 changes: 21 additions & 81 deletions packages/react-router/lib/types/route-module-annotations.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -67,94 +67,34 @@ type HeadersArgs = {
errorHeaders: Headers | undefined;
};

type ClientDataFunctionArgs<T extends RouteInfo> = {
/**
* 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
* `<HydratedRouter unstable_getContext>` prop
*/
context: unstable_RouterContextProvider;
};

type ServerDataFunctionArgs<T extends RouteInfo> = {
/** 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<T extends RouteInfo> = (
args: ServerDataFunctionArgs<T>,
args: ServerDataFunctionArgs<T["params"]>,
next: unstable_MiddlewareNextFunction<Response>
) => MaybePromise<Response | void>;

type CreateClientMiddlewareFunction<T extends RouteInfo> = (
args: ClientDataFunctionArgs<T>,
args: ClientDataFunctionArgs<T["params"]>,
next: unstable_MiddlewareNextFunction<undefined>
) => MaybePromise<void>;

type CreateServerLoaderArgs<T extends RouteInfo> = ServerDataFunctionArgs<T>;
type CreateServerLoaderArgs<T extends RouteInfo> = ServerDataFunctionArgs<
T["params"]
>;

type CreateClientLoaderArgs<T extends RouteInfo> = ClientDataFunctionArgs<T> & {
type CreateClientLoaderArgs<T extends RouteInfo> = 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<ServerDataFrom<T["module"]["loader"]>>;
};

type CreateServerActionArgs<T extends RouteInfo> = ServerDataFunctionArgs<T>;
type CreateServerActionArgs<T extends RouteInfo> = ServerDataFunctionArgs<
T["params"]
>;

type CreateClientActionArgs<T extends RouteInfo> = ClientDataFunctionArgs<T> & {
type CreateClientActionArgs<T extends RouteInfo> = 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<ServerDataFrom<T["module"]["action"]>>;
};
Expand Down