Have router
export const router = createBrowserRouter([
{
path: '/todos/:todoId',
element: <Todo />,
loader: todoLoader,
}
]);
Have loader
export const loader: LoaderFunction = async ({ params }) => {
return await fetchData(params.todoId);
};
How to type params according to path?
Waiting for the highlighting of the specified parameters in the path.
UPDATE: Thanks @DrewReese @LindaPaiste came to the following solution.
const PathNames = {
todoDetail: '/todos/:idTodo',
} as const;
interface Args extends ActionFunctionArgs {
params: Params<ParamParseKey<typeof PathNames.todoDetail>>;
}
export const loader: LoaderFunction = async ({ params }:Args) => {
return await fetchData(params.todoId);
};
declare type _PathParam<Path extends string> = Path extends `${infer L}/${infer R}` ? _PathParam<L> | _PathParam<R> : Path extends `:${infer Param}` ? Param extends `${infer Optional}?` ? Optional : Param : never; /** * Examples: * "/a/b/*" -> "*" * ":a" -> "a" * "/a/:b" -> "b" * "/a/blahblahblah:b" -> "b" * "/:a/:b" -> "a" | "b" * "/:a/b/:c/*" -> "a" | "c" | "*" */ declare type PathParam<Path extends string> = Path extends "*" ? "*" : Path extends `${infer Rest}/*` ? "*" | _PathParam<Rest> : _PathParam<Path>; export declare type ParamParseKey<Segment extends string> = [ PathParam<Segment> ] extends [never] ? string : PathParam<Segment>; /** * The parameters that were parsed from the URL path. */ export declare type Params<Key extends string = string> = { readonly [key in Key]: string | undefined; };
Declare a map/object of paths (key - value) that you can use RRD's Params
and ParamParseKey
utility types to extract the route path parameters (Credit to LindaPaiste and Qvazi for method).
const Paths = {
todoDetail: "/todos/:idTodo",
} as const;
interface TodoLoaderArgs extends ActionFunctionArgs {
params: Params<ParamParseKey<typeof Paths.todoDetail>>;
}
const todoLoader: LoaderFunction = async ({ params }: TodoLoaderArgs) => {
return await fetchData(params.idTodo);
};
const router = createBrowserRouter([
{
path: Paths.todoDetail,
element: <Todo />,
loader: todoLoader as LoaderFunction
},
]);
However, the fetchData
function expects a string
type, so there's an incompatibility here since params.idTodo
is typed as string | undefined
. It's fixed by doing a conditional check before accessing
const todoLoader: LoaderFunction = async ({ params }: TodoLoaderArgs) => {
return params.idTodo ? await fetchData(params.idTodo) : null;
};
or asserting it's non-null, e.g. fetchData(params.idTodo!)
or you can provide a fallback, e.g. fetchData(params.idTodo ?? "")
. Perhaps the fallback can be some default query parameter value for the data fetching.
const todoLoader: LoaderFunction = async ({ params }: TodoLoaderArgs) => {
return await fetchData(params.idTodo ?? "");
};
It's a bit of a roundabout method, but this appears to be correctly typed and working in a running codesandbox, might be a bit "hackish" though (my Typescript foo is not great). The gist is that you need to override the loader function args
parameter so you can override the params
property to include the path params you want to access in the loader.
The loader definitions to override:
/** * The parameters that were parsed from the URL path. */ export declare type Params<Key extends string = string> = { readonly [key in Key]: string | undefined; }; /** * @private * 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 { request: Request; params: Params; context?: any; } /** * Arguments passed to loader functions */ export interface LoaderFunctionArgs extends DataFunctionArgs { } /** * Route loader function signature */ export interface LoaderFunction { (args: LoaderFunctionArgs): Promise<Response> | Response | Promise<any> | any; }
New interface declarations and usage:
import {
RouterProvider,
createBrowserRouter,
Navigate,
useLoaderData,
LoaderFunction,
LoaderFunctionArgs
} from "react-router-dom";
interface TodoLoaderFunctionArgs extends Omit<LoaderFunctionArgs, "params"> {
params: {
todoId: string;
};
}
interface TodoLoaderFunction extends Omit<LoaderFunction, "args"> {
(args: TodoLoaderFunctionArgs):
| Promise<Response>
| Response
| Promise<any>
| any;
}
const todoLoader: TodoLoaderFunction = async ({ params }) => {
return await fetchData(params.todoId);
};
const router = createBrowserRouter([
{
path: "/todos/:todoId",
element: <Todo />,
loader: todoLoader as LoaderFunction
},
]);
An alternative, which might be a little simpler, would be to simply recast the params
prop.
interface Params {
todoId: string
}
const todoLoader: LoaderFunction = async ({ params }) => {
const typedParams = params as unknown as Params;
return await fetchData(typedParams.todoId);
};