Skip to content

Commit

Permalink
Add generics for loader data, action data, and fetchers (#12180)
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 authored Oct 23, 2024
1 parent 49ffd53 commit 5c7cc02
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 31 deletions.
15 changes: 15 additions & 0 deletions .changeset/long-peas-doubt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"react-router": major
---

Migrate Remix type generics to React Router

- These generics are provided for Remix v2 migration purposes
- These generics and the APIs they exist on should be considered informally deprecated in favor of the new `Route.*` types
- Anyone migrating from React Router v6 should probably not leverage these new generics and should migrate straight to the `Route.*` types
- For React Router v6 users, these generics are new and should not impact your app, with one exception
- `useFetcher` previously had an optional generic (used primarily by Remix v2) that expected the data type
- This has been updated in v7 to expect the type of the function that generates the data (i.e., `typeof loader`/`typeof action`)
- Therefore, you should update your usages:
-`useFetcher<LoaderData>()`
-`useFetcher<typeof loader>()`
14 changes: 9 additions & 5 deletions packages/react-router/lib/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -817,14 +817,14 @@ export function Routes({
return useRoutes(createRoutesFromChildren(children), location);
}

export interface AwaitResolveRenderFunction {
(data: Awaited<any>): React.ReactNode;
export interface AwaitResolveRenderFunction<Resolve = any> {
(data: Awaited<Resolve>): React.ReactNode;
}

/**
* @category Types
*/
export interface AwaitProps {
export interface AwaitProps<Resolve> {
/**
When using a function, the resolved value is provided as the parameter.
Expand Down Expand Up @@ -923,7 +923,7 @@ export interface AwaitProps {
}
```
*/
resolve: TrackedPromise | any;
resolve: Resolve;
}

/**
Expand Down Expand Up @@ -967,7 +967,11 @@ function Book() {
@category Components
*/
export function Await({ children, errorElement, resolve }: AwaitProps) {
export function Await<Resolve>({
children,
errorElement,
resolve,
}: AwaitProps<Resolve>) {
return (
<AwaitErrorBoundary resolve={resolve} errorElement={errorElement}>
<ResolveAwait>{children}</ResolveAwait>
Expand Down
5 changes: 3 additions & 2 deletions packages/react-router/lib/dom/lib.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import {
useResolvedPath,
useRouteId,
} from "../hooks";
import type { SerializeFrom } from "../types";

////////////////////////////////////////////////////////////////////////////////
//#region Global Stuff
Expand Down Expand Up @@ -1792,7 +1793,7 @@ export type FetcherWithComponents<TData> = Fetcher<TData> & {
@category Hooks
*/
export function useFetcher<TData = any>({
export function useFetcher<T = any>({
key,
}: {
/**
Expand All @@ -1813,7 +1814,7 @@ export function useFetcher<TData = any>({
```
*/
key?: string;
} = {}): FetcherWithComponents<TData> {
} = {}): FetcherWithComponents<SerializeFrom<T>> {
let { router } = useDataRouterContext(DataRouterHook.UseFetcher);
let state = useDataRouterState(DataRouterStateHook.UseFetcher);
let fetcherData = React.useContext(FetchersContext);
Expand Down
3 changes: 0 additions & 3 deletions packages/react-router/lib/dom/ssr/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ import { useLocation } from "../../hooks";
import { getPartialManifest, isFogOfWarEnabled } from "./fog-of-war";
import type { PageLinkDescriptor } from "../../router/links";

// TODO: Temporary shim until we figure out the way to handle typings in v7
export type SerializeFrom<D> = D extends () => {} ? Awaited<ReturnType<D>> : D;

function useDataRouterContext() {
let context = React.useContext(DataRouterContext);
invariant(
Expand Down
34 changes: 19 additions & 15 deletions packages/react-router/lib/dom/ssr/routeModules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import type {
ShouldRevalidateFunction,
} from "../../router/utils";

import type { SerializeFrom } from "./components";
import type { EntryRoute } from "./routes";
import type { DataRouteMatch } from "../../context";
import type { LinkDescriptor } from "../../router/links";
import type { SerializeFrom } from "../../types";

export interface RouteModules {
[routeId: string]: RouteModule | undefined;
Expand Down Expand Up @@ -96,22 +96,24 @@ export interface LinksFunction {

export interface MetaMatch<
RouteId extends string = string,
Loader extends LoaderFunction | unknown = unknown
Loader extends LoaderFunction | ClientLoaderFunction | unknown = unknown
> {
id: RouteId;
pathname: DataRouteMatch["pathname"];
data: Loader extends LoaderFunction ? SerializeFrom<Loader> : unknown;
data: Loader extends LoaderFunction | ClientLoaderFunction
? SerializeFrom<Loader>
: unknown;
handle?: RouteHandle;
params: DataRouteMatch["params"];
meta: MetaDescriptor[];
error?: unknown;
}

export type MetaMatches<
MatchLoaders extends Record<string, LoaderFunction | unknown> = Record<
MatchLoaders extends Record<
string,
unknown
>
LoaderFunction | ClientLoaderFunction | unknown
> = Record<string, unknown>
> = Array<
{
[K in keyof MatchLoaders]: MetaMatch<
Expand All @@ -122,14 +124,16 @@ export type MetaMatches<
>;

export interface MetaArgs<
Loader extends LoaderFunction | unknown = unknown,
MatchLoaders extends Record<string, LoaderFunction | unknown> = Record<
Loader extends LoaderFunction | ClientLoaderFunction | unknown = unknown,
MatchLoaders extends Record<
string,
unknown
>
LoaderFunction | ClientLoaderFunction | unknown
> = Record<string, unknown>
> {
data:
| (Loader extends LoaderFunction ? SerializeFrom<Loader> : unknown)
| (Loader extends LoaderFunction | ClientLoaderFunction
? SerializeFrom<Loader>
: unknown)
| undefined;
params: Params;
location: Location;
Expand Down Expand Up @@ -188,11 +192,11 @@ export interface MetaArgs<
* ```
*/
export interface MetaFunction<
Loader extends LoaderFunction | unknown = unknown,
MatchLoaders extends Record<string, LoaderFunction | unknown> = Record<
Loader extends LoaderFunction | ClientLoaderFunction | unknown = unknown,
MatchLoaders extends Record<
string,
unknown
>
LoaderFunction | ClientLoaderFunction | unknown
> = Record<string, unknown>
> {
(args: MetaArgs<Loader, MatchLoaders>): MetaDescriptor[] | undefined;
}
Expand Down
17 changes: 11 additions & 6 deletions packages/react-router/lib/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
resolveTo,
stripBasename,
} from "./router/utils";
import type { SerializeFrom } from "./types";

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

/**
Expand Down Expand Up @@ -1115,9 +1116,11 @@ export function useLoaderData(): unknown {
@category Hooks
*/
export function useRouteLoaderData(routeId: string): unknown {
export function useRouteLoaderData<T = any>(
routeId: string
): SerializeFrom<T> | undefined {
let state = useDataRouterState(DataRouterStateHook.UseRouteLoaderData);
return state.loaderData[routeId];
return state.loaderData[routeId] as SerializeFrom<T> | undefined;
}

/**
Expand Down Expand Up @@ -1145,10 +1148,12 @@ export function useRouteLoaderData(routeId: string): unknown {
@category Hooks
*/
export function useActionData(): unknown {
export function useActionData<T = any>(): SerializeFrom<T> | undefined {
let state = useDataRouterState(DataRouterStateHook.UseActionData);
let routeId = useCurrentRouteId(DataRouterStateHook.UseLoaderData);
return state.actionData ? state.actionData[routeId] : undefined;
return (state.actionData ? state.actionData[routeId] : undefined) as
| SerializeFrom<T>
| undefined;
}

/**
Expand Down
15 changes: 15 additions & 0 deletions packages/react-router/lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import type {
ClientLoaderFunctionArgs,
ClientActionFunctionArgs,
} from "./dom/ssr/routeModules";
import type { DataWithResponseInit } from "./router/utils";
import type { AppLoadContext } from "./server-runtime/data";
import type { Serializable } from "./server-runtime/single-fetch";
Expand Down Expand Up @@ -121,6 +125,17 @@ type Serialize<T> =

undefined

/**
* @deprecated Generics on data APIs such as `useLoaderData`, `useActionData`,
* `meta`, etc. are deprecated in favor of the `Route.*` types generated via
* `react-router typegen`
*/
export type SerializeFrom<T> = T extends (...args: infer Args) => unknown
? Args extends [ClientLoaderFunctionArgs | ClientActionFunctionArgs]
? ClientData<DataFrom<T>>
: ServerData<DataFrom<T>>
: T;

export type CreateServerLoaderArgs<Params> = ServerDataFunctionArgs<Params>;

export type CreateClientLoaderArgs<
Expand Down

0 comments on commit 5c7cc02

Please sign in to comment.