Skip to content

Commit 5c7cc02

Browse files
authored
Add generics for loader data, action data, and fetchers (#12180)
1 parent 49ffd53 commit 5c7cc02

File tree

7 files changed

+72
-31
lines changed

7 files changed

+72
-31
lines changed

.changeset/long-peas-doubt.md

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"react-router": major
3+
---
4+
5+
Migrate Remix type generics to React Router
6+
7+
- These generics are provided for Remix v2 migration purposes
8+
- These generics and the APIs they exist on should be considered informally deprecated in favor of the new `Route.*` types
9+
- Anyone migrating from React Router v6 should probably not leverage these new generics and should migrate straight to the `Route.*` types
10+
- For React Router v6 users, these generics are new and should not impact your app, with one exception
11+
- `useFetcher` previously had an optional generic (used primarily by Remix v2) that expected the data type
12+
- This has been updated in v7 to expect the type of the function that generates the data (i.e., `typeof loader`/`typeof action`)
13+
- Therefore, you should update your usages:
14+
-`useFetcher<LoaderData>()`
15+
-`useFetcher<typeof loader>()`

packages/react-router/lib/components.tsx

+9-5
Original file line numberDiff line numberDiff line change
@@ -817,14 +817,14 @@ export function Routes({
817817
return useRoutes(createRoutesFromChildren(children), location);
818818
}
819819

820-
export interface AwaitResolveRenderFunction {
821-
(data: Awaited<any>): React.ReactNode;
820+
export interface AwaitResolveRenderFunction<Resolve = any> {
821+
(data: Awaited<Resolve>): React.ReactNode;
822822
}
823823

824824
/**
825825
* @category Types
826826
*/
827-
export interface AwaitProps {
827+
export interface AwaitProps<Resolve> {
828828
/**
829829
When using a function, the resolved value is provided as the parameter.
830830
@@ -923,7 +923,7 @@ export interface AwaitProps {
923923
}
924924
```
925925
*/
926-
resolve: TrackedPromise | any;
926+
resolve: Resolve;
927927
}
928928

929929
/**
@@ -967,7 +967,11 @@ function Book() {
967967
@category Components
968968
969969
*/
970-
export function Await({ children, errorElement, resolve }: AwaitProps) {
970+
export function Await<Resolve>({
971+
children,
972+
errorElement,
973+
resolve,
974+
}: AwaitProps<Resolve>) {
971975
return (
972976
<AwaitErrorBoundary resolve={resolve} errorElement={errorElement}>
973977
<ResolveAwait>{children}</ResolveAwait>

packages/react-router/lib/dom/lib.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ import {
8888
useResolvedPath,
8989
useRouteId,
9090
} from "../hooks";
91+
import type { SerializeFrom } from "../types";
9192

9293
////////////////////////////////////////////////////////////////////////////////
9394
//#region Global Stuff
@@ -1792,7 +1793,7 @@ export type FetcherWithComponents<TData> = Fetcher<TData> & {
17921793
17931794
@category Hooks
17941795
*/
1795-
export function useFetcher<TData = any>({
1796+
export function useFetcher<T = any>({
17961797
key,
17971798
}: {
17981799
/**
@@ -1813,7 +1814,7 @@ export function useFetcher<TData = any>({
18131814
```
18141815
*/
18151816
key?: string;
1816-
} = {}): FetcherWithComponents<TData> {
1817+
} = {}): FetcherWithComponents<SerializeFrom<T>> {
18171818
let { router } = useDataRouterContext(DataRouterHook.UseFetcher);
18181819
let state = useDataRouterState(DataRouterStateHook.UseFetcher);
18191820
let fetcherData = React.useContext(FetchersContext);

packages/react-router/lib/dom/ssr/components.tsx

-3
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,6 @@ import { useLocation } from "../../hooks";
3232
import { getPartialManifest, isFogOfWarEnabled } from "./fog-of-war";
3333
import type { PageLinkDescriptor } from "../../router/links";
3434

35-
// TODO: Temporary shim until we figure out the way to handle typings in v7
36-
export type SerializeFrom<D> = D extends () => {} ? Awaited<ReturnType<D>> : D;
37-
3835
function useDataRouterContext() {
3936
let context = React.useContext(DataRouterContext);
4037
invariant(

packages/react-router/lib/dom/ssr/routeModules.ts

+19-15
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import type {
99
ShouldRevalidateFunction,
1010
} from "../../router/utils";
1111

12-
import type { SerializeFrom } from "./components";
1312
import type { EntryRoute } from "./routes";
1413
import type { DataRouteMatch } from "../../context";
1514
import type { LinkDescriptor } from "../../router/links";
15+
import type { SerializeFrom } from "../../types";
1616

1717
export interface RouteModules {
1818
[routeId: string]: RouteModule | undefined;
@@ -96,22 +96,24 @@ export interface LinksFunction {
9696

9797
export interface MetaMatch<
9898
RouteId extends string = string,
99-
Loader extends LoaderFunction | unknown = unknown
99+
Loader extends LoaderFunction | ClientLoaderFunction | unknown = unknown
100100
> {
101101
id: RouteId;
102102
pathname: DataRouteMatch["pathname"];
103-
data: Loader extends LoaderFunction ? SerializeFrom<Loader> : unknown;
103+
data: Loader extends LoaderFunction | ClientLoaderFunction
104+
? SerializeFrom<Loader>
105+
: unknown;
104106
handle?: RouteHandle;
105107
params: DataRouteMatch["params"];
106108
meta: MetaDescriptor[];
107109
error?: unknown;
108110
}
109111

110112
export type MetaMatches<
111-
MatchLoaders extends Record<string, LoaderFunction | unknown> = Record<
113+
MatchLoaders extends Record<
112114
string,
113-
unknown
114-
>
115+
LoaderFunction | ClientLoaderFunction | unknown
116+
> = Record<string, unknown>
115117
> = Array<
116118
{
117119
[K in keyof MatchLoaders]: MetaMatch<
@@ -122,14 +124,16 @@ export type MetaMatches<
122124
>;
123125

124126
export interface MetaArgs<
125-
Loader extends LoaderFunction | unknown = unknown,
126-
MatchLoaders extends Record<string, LoaderFunction | unknown> = Record<
127+
Loader extends LoaderFunction | ClientLoaderFunction | unknown = unknown,
128+
MatchLoaders extends Record<
127129
string,
128-
unknown
129-
>
130+
LoaderFunction | ClientLoaderFunction | unknown
131+
> = Record<string, unknown>
130132
> {
131133
data:
132-
| (Loader extends LoaderFunction ? SerializeFrom<Loader> : unknown)
134+
| (Loader extends LoaderFunction | ClientLoaderFunction
135+
? SerializeFrom<Loader>
136+
: unknown)
133137
| undefined;
134138
params: Params;
135139
location: Location;
@@ -188,11 +192,11 @@ export interface MetaArgs<
188192
* ```
189193
*/
190194
export interface MetaFunction<
191-
Loader extends LoaderFunction | unknown = unknown,
192-
MatchLoaders extends Record<string, LoaderFunction | unknown> = Record<
195+
Loader extends LoaderFunction | ClientLoaderFunction | unknown = unknown,
196+
MatchLoaders extends Record<
193197
string,
194-
unknown
195-
>
198+
LoaderFunction | ClientLoaderFunction | unknown
199+
> = Record<string, unknown>
196200
> {
197201
(args: MetaArgs<Loader, MatchLoaders>): MetaDescriptor[] | undefined;
198202
}

packages/react-router/lib/hooks.tsx

+11-6
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {
4848
resolveTo,
4949
stripBasename,
5050
} from "./router/utils";
51+
import type { SerializeFrom } from "./types";
5152

5253
// TODO: Let's get this back to using an import map and development/production
5354
// condition once we get the rollup build replaced
@@ -1082,10 +1083,10 @@ export function useMatches(): UIMatch[] {
10821083
10831084
@category Hooks
10841085
*/
1085-
export function useLoaderData(): unknown {
1086+
export function useLoaderData<T = any>(): SerializeFrom<T> {
10861087
let state = useDataRouterState(DataRouterStateHook.UseLoaderData);
10871088
let routeId = useCurrentRouteId(DataRouterStateHook.UseLoaderData);
1088-
return state.loaderData[routeId];
1089+
return state.loaderData[routeId] as SerializeFrom<T>;
10891090
}
10901091

10911092
/**
@@ -1115,9 +1116,11 @@ export function useLoaderData(): unknown {
11151116
11161117
@category Hooks
11171118
*/
1118-
export function useRouteLoaderData(routeId: string): unknown {
1119+
export function useRouteLoaderData<T = any>(
1120+
routeId: string
1121+
): SerializeFrom<T> | undefined {
11191122
let state = useDataRouterState(DataRouterStateHook.UseRouteLoaderData);
1120-
return state.loaderData[routeId];
1123+
return state.loaderData[routeId] as SerializeFrom<T> | undefined;
11211124
}
11221125

11231126
/**
@@ -1145,10 +1148,12 @@ export function useRouteLoaderData(routeId: string): unknown {
11451148
11461149
@category Hooks
11471150
*/
1148-
export function useActionData(): unknown {
1151+
export function useActionData<T = any>(): SerializeFrom<T> | undefined {
11491152
let state = useDataRouterState(DataRouterStateHook.UseActionData);
11501153
let routeId = useCurrentRouteId(DataRouterStateHook.UseLoaderData);
1151-
return state.actionData ? state.actionData[routeId] : undefined;
1154+
return (state.actionData ? state.actionData[routeId] : undefined) as
1155+
| SerializeFrom<T>
1156+
| undefined;
11521157
}
11531158

11541159
/**

packages/react-router/lib/types.ts

+15
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import type {
2+
ClientLoaderFunctionArgs,
3+
ClientActionFunctionArgs,
4+
} from "./dom/ssr/routeModules";
15
import type { DataWithResponseInit } from "./router/utils";
26
import type { AppLoadContext } from "./server-runtime/data";
37
import type { Serializable } from "./server-runtime/single-fetch";
@@ -121,6 +125,17 @@ type Serialize<T> =
121125

122126
undefined
123127

128+
/**
129+
* @deprecated Generics on data APIs such as `useLoaderData`, `useActionData`,
130+
* `meta`, etc. are deprecated in favor of the `Route.*` types generated via
131+
* `react-router typegen`
132+
*/
133+
export type SerializeFrom<T> = T extends (...args: infer Args) => unknown
134+
? Args extends [ClientLoaderFunctionArgs | ClientActionFunctionArgs]
135+
? ClientData<DataFrom<T>>
136+
: ServerData<DataFrom<T>>
137+
: T;
138+
124139
export type CreateServerLoaderArgs<Params> = ServerDataFunctionArgs<Params>;
125140

126141
export type CreateClientLoaderArgs<

0 commit comments

Comments
 (0)