Skip to content

Commit b6614b0

Browse files
authoredSep 5, 2023
Add generics for Remix type enhancements (#10843)
1 parent 3069e52 commit b6614b0

File tree

11 files changed

+67
-53
lines changed

11 files changed

+67
-53
lines changed
 

‎.changeset/align-types.md

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"react-router-dom": patch
3+
"react-router": patch
4+
"@remix-run/router": patch
5+
---
6+
7+
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.
8+
9+
- `Location` now accepts a generic for the `location.state` value
10+
- `ActionFunctionArgs`/`ActionFunction`/`LoaderFunctionArgs`/`LoaderFunction` now accept a generic for the `context` parameter (only used in SSR usages via `createStaticHandler`)
11+
- 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`

‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@
110110
},
111111
"filesize": {
112112
"packages/router/dist/router.umd.min.js": {
113-
"none": "47.2 kB"
113+
"none": "47.3 kB"
114114
},
115115
"packages/react-router/dist/react-router.production.min.js": {
116116
"none": "13.9 kB"

‎packages/react-router-dom-v5-compat/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export type {
104104
SubmitOptions,
105105
To,
106106
URLSearchParamsInit,
107+
UIMatch,
107108
unstable_Blocker,
108109
unstable_BlockerFunction,
109110
} from "./react-router-dom";

‎packages/react-router-dom/index.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ export type {
130130
ShouldRevalidateFunction,
131131
ShouldRevalidateFunctionArgs,
132132
To,
133+
UIMatch,
133134
} from "react-router";
134135
export {
135136
AbortedDeferredError,
@@ -1218,6 +1219,8 @@ export type FetcherWithComponents<TData> = Fetcher<TData> & {
12181219
load: (href: string) => void;
12191220
};
12201221

1222+
// TODO: (v7) Change the useFetcher generic default from `any` to `unknown`
1223+
12211224
/**
12221225
* Interacts with route loaders and actions without causing a navigation. Great
12231226
* for any interaction that stays on the same page.

‎packages/react-router-native/index.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export type {
6565
ShouldRevalidateFunction,
6666
ShouldRevalidateFunctionArgs,
6767
To,
68+
UIMatch,
6869
} from "react-router";
6970
export {
7071
AbortedDeferredError,

‎packages/react-router/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import type {
2626
ShouldRevalidateFunction,
2727
ShouldRevalidateFunctionArgs,
2828
To,
29+
UIMatch,
2930
} from "@remix-run/router";
3031
import {
3132
AbortedDeferredError,
@@ -167,6 +168,7 @@ export type {
167168
ShouldRevalidateFunction,
168169
ShouldRevalidateFunctionArgs,
169170
To,
171+
UIMatch,
170172
Blocker as unstable_Blocker,
171173
BlockerFunction as unstable_BlockerFunction,
172174
};

‎packages/react-router/lib/hooks.tsx

+4-15
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import type {
1212
Router as RemixRouter,
1313
RevalidationState,
1414
To,
15+
UIMatch,
1516
} from "@remix-run/router";
1617
import {
1718
IDLE_BLOCKER,
1819
Action as NavigationType,
20+
UNSAFE_convertRouteMatchToUiMatch as convertRouteMatchToUiMatch,
1921
UNSAFE_getPathContributingMatches as getPathContributingMatches,
2022
UNSAFE_invariant as invariant,
2123
isRouteErrorResponse,
@@ -834,25 +836,12 @@ export function useRevalidator() {
834836
* Returns the active route matches, useful for accessing loaderData for
835837
* parent/child routes or the route "handle" property
836838
*/
837-
export function useMatches() {
839+
export function useMatches(): UIMatch[] {
838840
let { matches, loaderData } = useDataRouterState(
839841
DataRouterStateHook.UseMatches
840842
);
841843
return React.useMemo(
842-
() =>
843-
matches.map((match) => {
844-
let { pathname, params } = match;
845-
// Note: This structure matches that created by createUseMatchesMatch
846-
// in the @remix-run/router , so if you change this please also change
847-
// that :) Eventually we'll DRY this up
848-
return {
849-
id: match.route.id,
850-
pathname,
851-
params,
852-
data: loaderData[match.route.id] as unknown,
853-
handle: match.route.handle as unknown,
854-
};
855-
}),
844+
() => matches.map((m) => convertRouteMatchToUiMatch(m, loaderData)),
856845
[matches, loaderData]
857846
);
858847
}

‎packages/router/history.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,18 @@ export interface Path {
4949
hash: string;
5050
}
5151

52+
// TODO: (v7) Change the Location generic default from `any` to `unknown` and
53+
// remove Remix `useLocation` wrapper.
54+
5255
/**
5356
* An entry in a history stack. A location contains information about the
5457
* URL path, as well as possibly some arbitrary state and a key.
5558
*/
56-
export interface Location extends Path {
59+
export interface Location<S = any> extends Path {
5760
/**
5861
* A value of arbitrary data associated with this location.
5962
*/
60-
state: any;
63+
state: S;
6164

6265
/**
6366
* A unique string associated with this location. May be used to safely store

‎packages/router/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export type {
2525
ShouldRevalidateFunction,
2626
ShouldRevalidateFunctionArgs,
2727
TrackedPromise,
28+
UIMatch,
2829
V7_FormMethod,
2930
} from "./utils";
3031

@@ -84,6 +85,7 @@ export {
8485
DeferredData as UNSAFE_DeferredData,
8586
ErrorResponseImpl as UNSAFE_ErrorResponseImpl,
8687
convertRoutesToDataRoutes as UNSAFE_convertRoutesToDataRoutes,
88+
convertRouteMatchToUiMatch as UNSAFE_convertRouteMatchToUiMatch,
8789
getPathContributingMatches as UNSAFE_getPathContributingMatches,
8890
} from "./utils";
8991

‎packages/router/router.ts

+4-27
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import type {
1111
ActionFunction,
1212
AgnosticDataRouteMatch,
1313
AgnosticDataRouteObject,
14-
AgnosticRouteMatch,
1514
AgnosticRouteObject,
1615
DataResult,
1716
DeferredData,
@@ -31,12 +30,14 @@ import type {
3130
ShouldRevalidateFunctionArgs,
3231
Submission,
3332
SuccessResult,
33+
UIMatch,
3434
V7_FormMethod,
3535
V7_MutationFormMethod,
3636
} from "./utils";
3737
import {
3838
ErrorResponseImpl,
3939
ResultType,
40+
convertRouteMatchToUiMatch,
4041
convertRoutesToDataRoutes,
4142
getPathContributingMatches,
4243
immutableRouteKeys,
@@ -394,20 +395,12 @@ export interface RouterSubscriber {
394395
(state: RouterState): void;
395396
}
396397

397-
interface UseMatchesMatch {
398-
id: string;
399-
pathname: string;
400-
params: AgnosticRouteMatch["params"];
401-
data: unknown;
402-
handle: unknown;
403-
}
404-
405398
/**
406399
* Function signature for determining the key to be used in scroll restoration
407400
* for a given location
408401
*/
409402
export interface GetScrollRestorationKeyFunction {
410-
(location: Location, matches: UseMatchesMatch[]): string | null;
403+
(location: Location, matches: UIMatch[]): string | null;
411404
}
412405

413406
/**
@@ -2461,7 +2454,7 @@ export function createRouter(init: RouterInit): Router {
24612454
if (getScrollRestorationKey) {
24622455
let key = getScrollRestorationKey(
24632456
location,
2464-
matches.map((m) => createUseMatchesMatch(m, state.loaderData))
2457+
matches.map((m) => convertRouteMatchToUiMatch(m, state.loaderData))
24652458
);
24662459
return key || location.key;
24672460
}
@@ -4332,22 +4325,6 @@ function hasNakedIndexQuery(search: string): boolean {
43324325
return new URLSearchParams(search).getAll("index").some((v) => v === "");
43334326
}
43344327

4335-
// Note: This should match the format exported by useMatches, so if you change
4336-
// this please also change that :) Eventually we'll DRY this up
4337-
function createUseMatchesMatch(
4338-
match: AgnosticDataRouteMatch,
4339-
loaderData: RouteData
4340-
): UseMatchesMatch {
4341-
let { route, pathname, params } = match;
4342-
return {
4343-
id: route.id,
4344-
pathname,
4345-
params,
4346-
data: loaderData[route.id] as unknown,
4347-
handle: route.handle as unknown,
4348-
};
4349-
}
4350-
43514328
function getTargetMatch(
43524329
matches: AgnosticDataRouteMatch[],
43534330
location: Location | string

‎packages/router/utils.ts

+33-8
Original file line numberDiff line numberDiff line change
@@ -137,21 +137,24 @@ export type Submission =
137137
* Arguments passed to route loader/action functions. Same for now but we keep
138138
* this as a private implementation detail in case they diverge in the future.
139139
*/
140-
interface DataFunctionArgs {
140+
interface DataFunctionArgs<Context> {
141141
request: Request;
142142
params: Params;
143-
context?: any;
143+
context?: Context;
144144
}
145145

146+
// TODO: (v7) Change the defaults from any to unknown in and remove Remix wrappers:
147+
// ActionFunction, ActionFunctionArgs, LoaderFunction, LoaderFunctionArgs
148+
146149
/**
147150
* Arguments passed to loader functions
148151
*/
149-
export interface LoaderFunctionArgs extends DataFunctionArgs {}
152+
export interface LoaderFunctionArgs<C = any> extends DataFunctionArgs<C> {}
150153

151154
/**
152155
* Arguments passed to action functions
153156
*/
154-
export interface ActionFunctionArgs extends DataFunctionArgs {}
157+
export interface ActionFunctionArgs<C = any> extends DataFunctionArgs<C> {}
155158

156159
/**
157160
* Loaders and actions can return anything except `undefined` (`null` is a
@@ -163,15 +166,15 @@ type DataFunctionValue = Response | NonNullable<unknown> | null;
163166
/**
164167
* Route loader function signature
165168
*/
166-
export interface LoaderFunction {
167-
(args: LoaderFunctionArgs): Promise<DataFunctionValue> | DataFunctionValue;
169+
export interface LoaderFunction<C = any> {
170+
(args: LoaderFunctionArgs<C>): Promise<DataFunctionValue> | DataFunctionValue;
168171
}
169172

170173
/**
171174
* Route action function signature
172175
*/
173-
export interface ActionFunction {
174-
(args: ActionFunctionArgs): Promise<DataFunctionValue> | DataFunctionValue;
176+
export interface ActionFunction<C = any> {
177+
(args: ActionFunctionArgs<C>): Promise<DataFunctionValue> | DataFunctionValue;
175178
}
176179

177180
/**
@@ -490,6 +493,28 @@ export function matchRoutes<
490493
return matches;
491494
}
492495

496+
export interface UIMatch<D = unknown, H = unknown> {
497+
id: string;
498+
pathname: string;
499+
params: AgnosticRouteMatch["params"];
500+
data: D;
501+
handle: H;
502+
}
503+
504+
export function convertRouteMatchToUiMatch(
505+
match: AgnosticDataRouteMatch,
506+
loaderData: RouteData
507+
): UIMatch {
508+
let { route, pathname, params } = match;
509+
return {
510+
id: route.id,
511+
pathname,
512+
params,
513+
data: loaderData[route.id],
514+
handle: route.handle,
515+
};
516+
}
517+
493518
interface RouteMeta<
494519
RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject
495520
> {

0 commit comments

Comments
 (0)
Please sign in to comment.