using React-Router V6 and I tried to strongly type the dataloader functions that are using params
and also the useLoaderData
hook.
So far I have to do the following that is ugly :
A- For useLoaderData
, need to force the returnType :
const profil = useLoaderData() as TProfil;
I guess it would be cleaner to create a generic hook like export declare function useLoaderData<T>(): T;
instead of export declare function useLoaderData(): unknown;
B- for the dataloader, what is the type of the params received ? Had to force to any
, but this is uggly. How to strongly type this and declare somewhere that params is made of "id" which comes from the parameter name in the Route definition ?
const careerDetailDataLoader = async ({ params }: any): Promise<TProfil> => {
const { id } = params;
const res = await fetch(`http://localhost:4000/careers/${id}`);
const data: TProfil = await res.json();
return data;
};
<Route path=":id" element={<CareerDetailsPage />} loader={careerDetailDataLoader} />
I encountered the same problems as you when I first used deferred loaders last night.
I came up with the following helper functions as a workaround for the lack of type safety when using react-router-dom
:
// utils.ts
import { Await as RrdAwait, defer, LoaderFunctionArgs, useLoaderData as useRrdLoaderData } from "react-router-dom";
export function useLoaderData<TLoader extends ReturnType<typeof deferredLoader>>() {
return useRrdLoaderData() as ReturnType<TLoader>["data"];
}
export function deferredLoader<TData extends Record<string, unknown>>(dataFunc: (args: LoaderFunctionArgs) => TData) {
return (args: LoaderFunctionArgs) =>
defer(dataFunc(args)) as Omit<ReturnType<typeof defer>, "data"> & { data: TData };
}
export interface AwaitResolveRenderFunction<T> {
(data: Awaited<T>): React.ReactElement;
}
export interface AwaitProps<T> {
children: React.ReactNode | AwaitResolveRenderFunction<T>;
errorElement?: React.ReactNode;
resolve: Promise<T>;
}
export function Await<T>(props: AwaitProps<T>): JSX.Element {
return RrdAwait(props);
}
Usage:
// MainLayout.tsx
import { Await, deferredLoader, useLoaderData } from "./utils";
export const mainLayoutLoader = deferredLoader(args => ({
metrics: api.metrics.get()
}));
export const MainLayout: FC = () => {
const data = useLoaderData<typeof mainLayoutLoader>();
return (
<Suspense fallback={<SiderMenu />}>
<Await resolve={data.metrics}>
{metrics => <SiderMenu metrics={metrics} />}
</Await>
</Suspense>
);
};
const router = createBrowserRouter([
{
path: "/",
element: <MainLayout />,
loader: mainLayoutLoader
}]);
Where:
api.metrics.get()
returns a type of Promise<Metric[]>
useLoaderData<typeof mainLayoutLoader>()
returns a type of { metrics: Promise<Metric[]>; }
metrics
inside {metrics => ...}
inside the <Await>
node will be typed as Metric[]
args
are of type LoaderFunctionArgs
as defined in react-router-dom
, which results a type of { [key: string]: string | undefined; }
for args.params