7

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} />
Jerome
  • 2,429
  • 2
  • 22
  • 37

2 Answers2

12

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
andreibsk
  • 121
  • 2
  • 3
0

First question

I guess it would be cleaner to create a generic hook like export declare function useLoaderData(): T; instead of export declare function useLoaderData(): unknown;

Any solution using generics only hides a necessary type cast using as so the generic version gives a false sense of type safety.

Remix has exactly the generic useLoaderData<T>() that you suggest. At the remix github repo, there is a detailed explanation of why they deprecated the generic version of the function and why the useLoaderData() function which they ported to react-router v6 does not use generics. (The suggested helper function in the other answer has the same problem as the deprecated function in remix.)

Second question

for the dataloader, what is the type of the params received ?

In react-router, there is ActionFunctionArgs which leads to Params being defined (in @remix-run/router/dist/utils.d.ts/Params) like this:

export type Params<Key extends string = string> = {
    readonly [key in Key]: string | undefined;
};

However, you might want to check out https://stackoverflow.com/a/76579584/5882233 for a similar question (and potentially a cleaner solution) about the argument types of the loader and action functions of react-router v6.

iron9
  • 397
  • 2
  • 12