Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add generics for loader data, action data, and fetchers #12180

Merged
merged 4 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed during testing that I'm not sure we ever handled meta types for clientLoader. I think as long as we always union with undefined this should be valid since meta would have no data on initial render for clientLoader-only routes.

Hybrid routes are trickier because meta would have data for the server loader on SSR, and on initial hydration and then would have clientLoader data subsequently - but I think that's just a userland solution via MetaFunction<typeof loader | typeof clientLoader>

> {
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