From ea5a5f20bc77fb516c5bd6ff6b32684d4b82cf72 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 5 Sep 2023 15:39:25 -0400 Subject: [PATCH 1/2] Add generics for Remix type enhancements --- .changeset/align-types.md | 11 ++++++ packages/react-router-dom-v5-compat/index.ts | 1 + packages/react-router-dom/index.tsx | 3 ++ packages/react-router-native/index.tsx | 1 + packages/react-router/index.ts | 2 + packages/react-router/lib/hooks.tsx | 19 ++------- packages/router/history.ts | 7 +++- packages/router/index.ts | 2 + packages/router/router.ts | 31 ++------------- packages/router/utils.ts | 41 ++++++++++++++++---- 10 files changed, 66 insertions(+), 52 deletions(-) create mode 100644 .changeset/align-types.md diff --git a/.changeset/align-types.md b/.changeset/align-types.md new file mode 100644 index 0000000000..defa4f9663 --- /dev/null +++ b/.changeset/align-types.md @@ -0,0 +1,11 @@ +--- +"react-router-dom": patch +"react-router": patch +"@remix-run/router": patch +--- + +In order to move towards stricter TypeScript support in the future, we're aiming to replace current usages of `any` with `unknown` on exposed typings for user-provided data. To do this in Remix v2 without introducing breaking changes in React Router v6, we have added generics to a number of shared types. These continue to default to `any` in React Router and are overridden with `unknown` in Remix. In React Router v7 we plan to move these to `unknown` as a breakjing change. + +- `Location` now accepts a generic for the `location.state` value +- `ActionFunctionArgs`/`ActionFunction`/`LoaderFunctionArgs`/`LoaderFunction` now accept a generic for the `context` parameter (only used in SSR usages via `createStaticHandler`) +- The return type of `useMatches` (now exported as `UIMatch`) accepts generics for `match.data` and `match.handle` - both of which were already set to `unknown` diff --git a/packages/react-router-dom-v5-compat/index.ts b/packages/react-router-dom-v5-compat/index.ts index 6b970d291e..c5b26e62f7 100644 --- a/packages/react-router-dom-v5-compat/index.ts +++ b/packages/react-router-dom-v5-compat/index.ts @@ -104,6 +104,7 @@ export type { SubmitOptions, To, URLSearchParamsInit, + UIMatch, unstable_Blocker, unstable_BlockerFunction, } from "./react-router-dom"; diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx index 10ad7887cf..e60e754e72 100644 --- a/packages/react-router-dom/index.tsx +++ b/packages/react-router-dom/index.tsx @@ -130,6 +130,7 @@ export type { ShouldRevalidateFunction, ShouldRevalidateFunctionArgs, To, + UIMatch, } from "react-router"; export { AbortedDeferredError, @@ -1218,6 +1219,8 @@ export type FetcherWithComponents = Fetcher & { load: (href: string) => void; }; +// TODO: (v7) Change the useFetcher generic default from `any` to `unknown` + /** * Interacts with route loaders and actions without causing a navigation. Great * for any interaction that stays on the same page. diff --git a/packages/react-router-native/index.tsx b/packages/react-router-native/index.tsx index e5d0941719..06aa33a8c1 100644 --- a/packages/react-router-native/index.tsx +++ b/packages/react-router-native/index.tsx @@ -65,6 +65,7 @@ export type { ShouldRevalidateFunction, ShouldRevalidateFunctionArgs, To, + UIMatch, } from "react-router"; export { AbortedDeferredError, diff --git a/packages/react-router/index.ts b/packages/react-router/index.ts index 7a72bda98b..4ac70a3422 100644 --- a/packages/react-router/index.ts +++ b/packages/react-router/index.ts @@ -26,6 +26,7 @@ import type { ShouldRevalidateFunction, ShouldRevalidateFunctionArgs, To, + UIMatch, } from "@remix-run/router"; import { AbortedDeferredError, @@ -167,6 +168,7 @@ export type { ShouldRevalidateFunction, ShouldRevalidateFunctionArgs, To, + UIMatch, Blocker as unstable_Blocker, BlockerFunction as unstable_BlockerFunction, }; diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index c51102228c..fd93013d46 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -12,10 +12,12 @@ import type { Router as RemixRouter, RevalidationState, To, + UIMatch, } from "@remix-run/router"; import { IDLE_BLOCKER, Action as NavigationType, + UNSAFE_convertRouteMatchToUiMatch as convertRouteMatchToUiMatch, UNSAFE_getPathContributingMatches as getPathContributingMatches, UNSAFE_invariant as invariant, isRouteErrorResponse, @@ -834,25 +836,12 @@ export function useRevalidator() { * Returns the active route matches, useful for accessing loaderData for * parent/child routes or the route "handle" property */ -export function useMatches() { +export function useMatches(): UIMatch[] { let { matches, loaderData } = useDataRouterState( DataRouterStateHook.UseMatches ); return React.useMemo( - () => - matches.map((match) => { - let { pathname, params } = match; - // Note: This structure matches that created by createUseMatchesMatch - // in the @remix-run/router , so if you change this please also change - // that :) Eventually we'll DRY this up - return { - id: match.route.id, - pathname, - params, - data: loaderData[match.route.id] as unknown, - handle: match.route.handle as unknown, - }; - }), + () => matches.map((m) => convertRouteMatchToUiMatch(m, loaderData)), [matches, loaderData] ); } diff --git a/packages/router/history.ts b/packages/router/history.ts index 8a9ef89e38..c3c982c0cd 100644 --- a/packages/router/history.ts +++ b/packages/router/history.ts @@ -49,15 +49,18 @@ export interface Path { hash: string; } +// TODO: (v7) Change the Location generic default from `any` to `unknown` and +// remove Remix `useLocation` wrapper. + /** * An entry in a history stack. A location contains information about the * URL path, as well as possibly some arbitrary state and a key. */ -export interface Location extends Path { +export interface Location extends Path { /** * A value of arbitrary data associated with this location. */ - state: any; + state: S; /** * A unique string associated with this location. May be used to safely store diff --git a/packages/router/index.ts b/packages/router/index.ts index d67784437d..55bc1646cd 100644 --- a/packages/router/index.ts +++ b/packages/router/index.ts @@ -25,6 +25,7 @@ export type { ShouldRevalidateFunction, ShouldRevalidateFunctionArgs, TrackedPromise, + UIMatch, V7_FormMethod, } from "./utils"; @@ -84,6 +85,7 @@ export { DeferredData as UNSAFE_DeferredData, ErrorResponseImpl as UNSAFE_ErrorResponseImpl, convertRoutesToDataRoutes as UNSAFE_convertRoutesToDataRoutes, + convertRouteMatchToUiMatch as UNSAFE_convertRouteMatchToUiMatch, getPathContributingMatches as UNSAFE_getPathContributingMatches, } from "./utils"; diff --git a/packages/router/router.ts b/packages/router/router.ts index 488ccc2b2b..a611128fe9 100644 --- a/packages/router/router.ts +++ b/packages/router/router.ts @@ -11,7 +11,6 @@ import type { ActionFunction, AgnosticDataRouteMatch, AgnosticDataRouteObject, - AgnosticRouteMatch, AgnosticRouteObject, DataResult, DeferredData, @@ -31,12 +30,14 @@ import type { ShouldRevalidateFunctionArgs, Submission, SuccessResult, + UIMatch, V7_FormMethod, V7_MutationFormMethod, } from "./utils"; import { ErrorResponseImpl, ResultType, + convertRouteMatchToUiMatch, convertRoutesToDataRoutes, getPathContributingMatches, immutableRouteKeys, @@ -394,20 +395,12 @@ export interface RouterSubscriber { (state: RouterState): void; } -interface UseMatchesMatch { - id: string; - pathname: string; - params: AgnosticRouteMatch["params"]; - data: unknown; - handle: unknown; -} - /** * Function signature for determining the key to be used in scroll restoration * for a given location */ export interface GetScrollRestorationKeyFunction { - (location: Location, matches: UseMatchesMatch[]): string | null; + (location: Location, matches: UIMatch[]): string | null; } /** @@ -2461,7 +2454,7 @@ export function createRouter(init: RouterInit): Router { if (getScrollRestorationKey) { let key = getScrollRestorationKey( location, - matches.map((m) => createUseMatchesMatch(m, state.loaderData)) + matches.map((m) => convertRouteMatchToUiMatch(m, state.loaderData)) ); return key || location.key; } @@ -4332,22 +4325,6 @@ function hasNakedIndexQuery(search: string): boolean { return new URLSearchParams(search).getAll("index").some((v) => v === ""); } -// Note: This should match the format exported by useMatches, so if you change -// this please also change that :) Eventually we'll DRY this up -function createUseMatchesMatch( - match: AgnosticDataRouteMatch, - loaderData: RouteData -): UseMatchesMatch { - let { route, pathname, params } = match; - return { - id: route.id, - pathname, - params, - data: loaderData[route.id] as unknown, - handle: route.handle as unknown, - }; -} - function getTargetMatch( matches: AgnosticDataRouteMatch[], location: Location | string diff --git a/packages/router/utils.ts b/packages/router/utils.ts index 8a37bbd776..e463cc0392 100644 --- a/packages/router/utils.ts +++ b/packages/router/utils.ts @@ -137,21 +137,24 @@ export type Submission = * Arguments passed to route loader/action functions. Same for now but we keep * this as a private implementation detail in case they diverge in the future. */ -interface DataFunctionArgs { +interface DataFunctionArgs { request: Request; params: Params; - context?: any; + context?: Context; } +// TODO: (v7) Change the defaults from any to unknown in and remove Remix wrappers: +// ActionFunction, ActionFunctionArgs, LoaderFunction, LoaderFunctionArgs + /** * Arguments passed to loader functions */ -export interface LoaderFunctionArgs extends DataFunctionArgs {} +export interface LoaderFunctionArgs extends DataFunctionArgs {} /** * Arguments passed to action functions */ -export interface ActionFunctionArgs extends DataFunctionArgs {} +export interface ActionFunctionArgs extends DataFunctionArgs {} /** * Loaders and actions can return anything except `undefined` (`null` is a @@ -163,15 +166,15 @@ type DataFunctionValue = Response | NonNullable | null; /** * Route loader function signature */ -export interface LoaderFunction { - (args: LoaderFunctionArgs): Promise | DataFunctionValue; +export interface LoaderFunction { + (args: LoaderFunctionArgs): Promise | DataFunctionValue; } /** * Route action function signature */ -export interface ActionFunction { - (args: ActionFunctionArgs): Promise | DataFunctionValue; +export interface ActionFunction { + (args: ActionFunctionArgs): Promise | DataFunctionValue; } /** @@ -490,6 +493,28 @@ export function matchRoutes< return matches; } +export interface UIMatch { + id: string; + pathname: string; + params: AgnosticRouteMatch["params"]; + data: D; + handle: H; +} + +export function convertRouteMatchToUiMatch( + match: AgnosticDataRouteMatch, + loaderData: RouteData +): UIMatch { + let { route, pathname, params } = match; + return { + id: route.id, + pathname, + params, + data: loaderData[route.id], + handle: route.handle, + }; +} + interface RouteMeta< RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject > { From 25622d59c7e8d2297895d2fbf6ef626fafa5c323 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 5 Sep 2023 15:48:32 -0400 Subject: [PATCH 2/2] Bump bundle --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b82d2eecdb..56e6f6eb10 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ }, "filesize": { "packages/router/dist/router.umd.min.js": { - "none": "47.2 kB" + "none": "47.3 kB" }, "packages/react-router/dist/react-router.production.min.js": { "none": "13.9 kB"