Search code examples
typescriptreact-router-domdataloader

React router V6.5 : how to strongly type data loaders?


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} />

Solution

  • 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