From ca9ec8d2f3d96c582ef9a9e11932256ad0c9555b Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 22 Oct 2024 17:29:35 -0400 Subject: [PATCH 1/4] Add generics for loader data, action data, and fetchers --- .changeset/long-peas-doubt.md | 15 +++++++++++++++ packages/react-router/lib/components.tsx | 14 +++++++++----- packages/react-router/lib/dom/lib.tsx | 5 +++-- .../react-router/lib/dom/ssr/components.tsx | 3 --- .../react-router/lib/dom/ssr/routeModules.ts | 14 +++++++++----- packages/react-router/lib/hooks.tsx | 19 +++++++++++++------ packages/react-router/lib/types.ts | 17 +++++++++++++++++ 7 files changed, 66 insertions(+), 21 deletions(-) create mode 100644 .changeset/long-peas-doubt.md diff --git a/.changeset/long-peas-doubt.md b/.changeset/long-peas-doubt.md new file mode 100644 index 0000000000..67d9fa1952 --- /dev/null +++ b/.changeset/long-peas-doubt.md @@ -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()` + - ✅ `useFetcher()` diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index 9aa96dd231..0e69f97fef 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -817,14 +817,14 @@ export function Routes({ return useRoutes(createRoutesFromChildren(children), location); } -export interface AwaitResolveRenderFunction { - (data: Awaited): React.ReactNode; +export interface AwaitResolveRenderFunction { + (data: Awaited): React.ReactNode; } /** * @category Types */ -export interface AwaitProps { +export interface AwaitProps { /** When using a function, the resolved value is provided as the parameter. @@ -923,7 +923,7 @@ export interface AwaitProps { } ``` */ - resolve: TrackedPromise | any; + resolve: Resolve; } /** @@ -967,7 +967,11 @@ function Book() { @category Components */ -export function Await({ children, errorElement, resolve }: AwaitProps) { +export function Await({ + children, + errorElement, + resolve, +}: AwaitProps) { return ( {children} diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx index f2ecb3ee97..b40a8edb71 100644 --- a/packages/react-router/lib/dom/lib.tsx +++ b/packages/react-router/lib/dom/lib.tsx @@ -88,6 +88,7 @@ import { useResolvedPath, useRouteId, } from "../hooks"; +import { DeprecatedSerializeFrom } from "../types"; //////////////////////////////////////////////////////////////////////////////// //#region Global Stuff @@ -1792,7 +1793,7 @@ export type FetcherWithComponents = Fetcher & { @category Hooks */ -export function useFetcher({ +export function useFetcher({ key, }: { /** @@ -1813,7 +1814,7 @@ export function useFetcher({ ``` */ key?: string; -} = {}): FetcherWithComponents { +} = {}): FetcherWithComponents> { let { router } = useDataRouterContext(DataRouterHook.UseFetcher); let state = useDataRouterState(DataRouterStateHook.UseFetcher); let fetcherData = React.useContext(FetchersContext); diff --git a/packages/react-router/lib/dom/ssr/components.tsx b/packages/react-router/lib/dom/ssr/components.tsx index e91636138f..f56ea11cb3 100644 --- a/packages/react-router/lib/dom/ssr/components.tsx +++ b/packages/react-router/lib/dom/ssr/components.tsx @@ -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 extends () => {} ? Awaited> : D; - function useDataRouterContext() { let context = React.useContext(DataRouterContext); invariant( diff --git a/packages/react-router/lib/dom/ssr/routeModules.ts b/packages/react-router/lib/dom/ssr/routeModules.ts index 5c3500faa4..690a965e24 100644 --- a/packages/react-router/lib/dom/ssr/routeModules.ts +++ b/packages/react-router/lib/dom/ssr/routeModules.ts @@ -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 { DeprecatedSerializeFrom } from "../../types"; export interface RouteModules { [routeId: string]: RouteModule | undefined; @@ -42,7 +42,7 @@ export type ClientActionFunction = ( * Arguments passed to a route `clientAction` function */ export type ClientActionFunctionArgs = ActionFunctionArgs & { - serverAction: () => Promise>; + serverAction: () => Promise>; }; /** @@ -58,7 +58,7 @@ export type ClientLoaderFunction = (( * Arguments passed to a route `clientLoader` function */ export type ClientLoaderFunctionArgs = LoaderFunctionArgs & { - serverLoader: () => Promise>; + serverLoader: () => Promise>; }; /** @@ -100,7 +100,9 @@ export interface MetaMatch< > { id: RouteId; pathname: DataRouteMatch["pathname"]; - data: Loader extends LoaderFunction ? SerializeFrom : unknown; + data: Loader extends LoaderFunction + ? DeprecatedSerializeFrom + : unknown; handle?: RouteHandle; params: DataRouteMatch["params"]; meta: MetaDescriptor[]; @@ -129,7 +131,9 @@ export interface MetaArgs< > > { data: - | (Loader extends LoaderFunction ? SerializeFrom : unknown) + | (Loader extends LoaderFunction + ? DeprecatedSerializeFrom + : unknown) | undefined; params: Params; location: Location; diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index 430e14674e..67f4ba394e 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -48,6 +48,7 @@ import { resolveTo, stripBasename, } from "./router/utils"; +import type { DeprecatedSerializeFrom } from "./types"; // TODO: Let's get this back to using an import map and development/production // condition once we get the rollup build replaced @@ -1082,10 +1083,10 @@ export function useMatches(): UIMatch[] { @category Hooks */ -export function useLoaderData(): unknown { +export function useLoaderData(): DeprecatedSerializeFrom { let state = useDataRouterState(DataRouterStateHook.UseLoaderData); let routeId = useCurrentRouteId(DataRouterStateHook.UseLoaderData); - return state.loaderData[routeId]; + return state.loaderData[routeId] as DeprecatedSerializeFrom; } /** @@ -1115,9 +1116,11 @@ export function useLoaderData(): unknown { @category Hooks */ -export function useRouteLoaderData(routeId: string): unknown { +export function useRouteLoaderData( + routeId: string +): DeprecatedSerializeFrom | undefined { let state = useDataRouterState(DataRouterStateHook.UseRouteLoaderData); - return state.loaderData[routeId]; + return state.loaderData[routeId] as DeprecatedSerializeFrom | undefined; } /** @@ -1145,10 +1148,14 @@ export function useRouteLoaderData(routeId: string): unknown { @category Hooks */ -export function useActionData(): unknown { +export function useActionData(): + | DeprecatedSerializeFrom + | 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 + | DeprecatedSerializeFrom + | undefined; } /** diff --git a/packages/react-router/lib/types.ts b/packages/react-router/lib/types.ts index 9198cf5244..9cdeb19d1a 100644 --- a/packages/react-router/lib/types.ts +++ b/packages/react-router/lib/types.ts @@ -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"; @@ -121,6 +125,19 @@ type Serialize = 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 DeprecatedSerializeFrom = T extends ( + ...args: infer Args +) => unknown + ? Args extends [ClientLoaderFunctionArgs | ClientActionFunctionArgs] + ? ClientData> + : ServerData> + : T; + export type CreateServerLoaderArgs = ServerDataFunctionArgs; export type CreateClientLoaderArgs< From 8e0c346e6947884df81b54264a3dc24b1067d304 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 23 Oct 2024 10:51:54 -0400 Subject: [PATCH 2/4] Fix lint issue --- packages/react-router/lib/dom/lib.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx index b40a8edb71..bb6e01ed96 100644 --- a/packages/react-router/lib/dom/lib.tsx +++ b/packages/react-router/lib/dom/lib.tsx @@ -88,7 +88,7 @@ import { useResolvedPath, useRouteId, } from "../hooks"; -import { DeprecatedSerializeFrom } from "../types"; +import type { DeprecatedSerializeFrom } from "../types"; //////////////////////////////////////////////////////////////////////////////// //#region Global Stuff From 8c0a1cafba77978e356f8715b0b30566d76da85c Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 23 Oct 2024 11:15:47 -0400 Subject: [PATCH 3/4] Add meta support for client loader --- .../react-router/lib/dom/ssr/routeModules.ts | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/react-router/lib/dom/ssr/routeModules.ts b/packages/react-router/lib/dom/ssr/routeModules.ts index 690a965e24..b17c4f4843 100644 --- a/packages/react-router/lib/dom/ssr/routeModules.ts +++ b/packages/react-router/lib/dom/ssr/routeModules.ts @@ -96,11 +96,11 @@ 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 + data: Loader extends LoaderFunction | ClientLoaderFunction ? DeprecatedSerializeFrom : unknown; handle?: RouteHandle; @@ -110,10 +110,10 @@ export interface MetaMatch< } export type MetaMatches< - MatchLoaders extends Record = Record< + MatchLoaders extends Record< string, - unknown - > + LoaderFunction | ClientLoaderFunction | unknown + > = Record > = Array< { [K in keyof MatchLoaders]: MetaMatch< @@ -124,14 +124,14 @@ export type MetaMatches< >; export interface MetaArgs< - Loader extends LoaderFunction | unknown = unknown, - MatchLoaders extends Record = Record< + Loader extends LoaderFunction | ClientLoaderFunction | unknown = unknown, + MatchLoaders extends Record< string, - unknown - > + LoaderFunction | ClientLoaderFunction | unknown + > = Record > { data: - | (Loader extends LoaderFunction + | (Loader extends LoaderFunction | ClientLoaderFunction ? DeprecatedSerializeFrom : unknown) | undefined; @@ -192,11 +192,11 @@ export interface MetaArgs< * ``` */ export interface MetaFunction< - Loader extends LoaderFunction | unknown = unknown, - MatchLoaders extends Record = Record< + Loader extends LoaderFunction | ClientLoaderFunction | unknown = unknown, + MatchLoaders extends Record< string, - unknown - > + LoaderFunction | ClientLoaderFunction | unknown + > = Record > { (args: MetaArgs): MetaDescriptor[] | undefined; } From 7e47853f548d14b281d3421931d10d9bac918eeb Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 23 Oct 2024 15:15:07 -0400 Subject: [PATCH 4/4] Rename back to SerializeFrom --- packages/react-router/lib/dom/lib.tsx | 4 ++-- .../react-router/lib/dom/ssr/routeModules.ts | 10 +++++----- packages/react-router/lib/hooks.tsx | 16 +++++++--------- packages/react-router/lib/types.ts | 4 +--- 4 files changed, 15 insertions(+), 19 deletions(-) diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx index bb6e01ed96..cc971d2361 100644 --- a/packages/react-router/lib/dom/lib.tsx +++ b/packages/react-router/lib/dom/lib.tsx @@ -88,7 +88,7 @@ import { useResolvedPath, useRouteId, } from "../hooks"; -import type { DeprecatedSerializeFrom } from "../types"; +import type { SerializeFrom } from "../types"; //////////////////////////////////////////////////////////////////////////////// //#region Global Stuff @@ -1814,7 +1814,7 @@ export function useFetcher({ ``` */ key?: string; -} = {}): FetcherWithComponents> { +} = {}): FetcherWithComponents> { let { router } = useDataRouterContext(DataRouterHook.UseFetcher); let state = useDataRouterState(DataRouterStateHook.UseFetcher); let fetcherData = React.useContext(FetchersContext); diff --git a/packages/react-router/lib/dom/ssr/routeModules.ts b/packages/react-router/lib/dom/ssr/routeModules.ts index b17c4f4843..5e3f607bf0 100644 --- a/packages/react-router/lib/dom/ssr/routeModules.ts +++ b/packages/react-router/lib/dom/ssr/routeModules.ts @@ -12,7 +12,7 @@ import type { import type { EntryRoute } from "./routes"; import type { DataRouteMatch } from "../../context"; import type { LinkDescriptor } from "../../router/links"; -import type { DeprecatedSerializeFrom } from "../../types"; +import type { SerializeFrom } from "../../types"; export interface RouteModules { [routeId: string]: RouteModule | undefined; @@ -42,7 +42,7 @@ export type ClientActionFunction = ( * Arguments passed to a route `clientAction` function */ export type ClientActionFunctionArgs = ActionFunctionArgs & { - serverAction: () => Promise>; + serverAction: () => Promise>; }; /** @@ -58,7 +58,7 @@ export type ClientLoaderFunction = (( * Arguments passed to a route `clientLoader` function */ export type ClientLoaderFunctionArgs = LoaderFunctionArgs & { - serverLoader: () => Promise>; + serverLoader: () => Promise>; }; /** @@ -101,7 +101,7 @@ export interface MetaMatch< id: RouteId; pathname: DataRouteMatch["pathname"]; data: Loader extends LoaderFunction | ClientLoaderFunction - ? DeprecatedSerializeFrom + ? SerializeFrom : unknown; handle?: RouteHandle; params: DataRouteMatch["params"]; @@ -132,7 +132,7 @@ export interface MetaArgs< > { data: | (Loader extends LoaderFunction | ClientLoaderFunction - ? DeprecatedSerializeFrom + ? SerializeFrom : unknown) | undefined; params: Params; diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index 67f4ba394e..68e7b9a8a5 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -48,7 +48,7 @@ import { resolveTo, stripBasename, } from "./router/utils"; -import type { DeprecatedSerializeFrom } from "./types"; +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 @@ -1083,10 +1083,10 @@ export function useMatches(): UIMatch[] { @category Hooks */ -export function useLoaderData(): DeprecatedSerializeFrom { +export function useLoaderData(): SerializeFrom { let state = useDataRouterState(DataRouterStateHook.UseLoaderData); let routeId = useCurrentRouteId(DataRouterStateHook.UseLoaderData); - return state.loaderData[routeId] as DeprecatedSerializeFrom; + return state.loaderData[routeId] as SerializeFrom; } /** @@ -1118,9 +1118,9 @@ export function useLoaderData(): DeprecatedSerializeFrom { */ export function useRouteLoaderData( routeId: string -): DeprecatedSerializeFrom | undefined { +): SerializeFrom | undefined { let state = useDataRouterState(DataRouterStateHook.UseRouteLoaderData); - return state.loaderData[routeId] as DeprecatedSerializeFrom | undefined; + return state.loaderData[routeId] as SerializeFrom | undefined; } /** @@ -1148,13 +1148,11 @@ export function useRouteLoaderData( @category Hooks */ -export function useActionData(): - | DeprecatedSerializeFrom - | undefined { +export function useActionData(): SerializeFrom | undefined { let state = useDataRouterState(DataRouterStateHook.UseActionData); let routeId = useCurrentRouteId(DataRouterStateHook.UseLoaderData); return (state.actionData ? state.actionData[routeId] : undefined) as - | DeprecatedSerializeFrom + | SerializeFrom | undefined; } diff --git a/packages/react-router/lib/types.ts b/packages/react-router/lib/types.ts index 9cdeb19d1a..0e1897df8a 100644 --- a/packages/react-router/lib/types.ts +++ b/packages/react-router/lib/types.ts @@ -130,9 +130,7 @@ type Serialize = * `meta`, etc. are deprecated in favor of the `Route.*` types generated via * `react-router typegen` */ -export type DeprecatedSerializeFrom = T extends ( - ...args: infer Args -) => unknown +export type SerializeFrom = T extends (...args: infer Args) => unknown ? Args extends [ClientLoaderFunctionArgs | ClientActionFunctionArgs] ? ClientData> : ServerData>