DEV Community

Francisco Sousa
Francisco Sousa

Posted on • Originally published at jfranciscosousa.com on

Typing remix loaders with confidence

Well, hello there, it's time for a lightning-quick Remix tip. Let's see how we can genuinely write typesafe Remix routes by sharing types between loaders and components for full-stack typing!

Remix, what?

For the readers unfamiliar with Remix, it's a React framework created by the react-router team. It uses react-router to make a server-rendering full-stack framework with React support. It's the other kind of Next.js (sorry).

Loaders, what are they?

Remix is a server-side rendering framework, and as such, you can load data directly to your components while they are being rendered on the server.

export function loader() {
  return "Hello world!";
}

export default function SomeRemixPage() {
  const data = useLoaderData();
  return <p>{ data }</p>;
}
Enter fullscreen mode Exit fullscreen mode

You can only define the loader function on the Remix route file, but you can then call the useLoaderData hook on every component used inside that route. This is very useful for better SEO and spares you from adding loading states to your app, as the page comes pre-rendered from the server.

Let's add types the regular way

You can quickly type useLoaderData using type variables. Its type signature is useLoaderData<T>: T, so if you do useLoaderData<string>, you just typed your loader!

export function loader(): string {
  return "Hello world!";
}

export default function SomeRemixPage() {
  const data = useLoaderData<string>();
  return <p>{ data }</p>;
}
Enter fullscreen mode Exit fullscreen mode

However, this has a couple of issues. Typing the generic T on useLoaderData is the same thing as doing this:

const data = useLoaderData<string>();
const data = useLoaderData() as string;
Enter fullscreen mode Exit fullscreen mode

If you do not type useLoaderData, its default type is any, so you can just cast that to whatever you want. This means that the following scenario won't report type errors and would just crash during runtime.

export function loader(): string {
  return "Hello world!";
}

export default function SomeRemixPage() {
  const { data } = useLoaderData<{data: string}>();
  return <p>{ data }</p>;
}
Enter fullscreen mode Exit fullscreen mode

In the above scenario, this will crash, even though the types are all valid. We want the loader data to be of type { data: string }, but as there is no direct-type connection between the loader and the hook, some bugs might leak into runtime if you type all of your loader and useLoaderData like this.

Inferring the types from the loader

The solution is to infer the types from the loader automatically. The first step is to never use the LoaderFunction type.

import { json } from "@remix-run/node"; // or "@remix-run/cloudflare"
import type { LoaderFunction } from "@remix-run/node"; // or "@remix-run/cloudflare"

export const loader: LoaderFunction = async () => {
    return json({ ok: true });
};
Enter fullscreen mode Exit fullscreen mode

As of Remix version 1.5.1 the LoaderFunction return type is Promise<Response> | Response | Promise<AppData> | AppData which means we cannot reliably use the solution I will propose. AppData is an internal Remix type that is the same as any, which doesn't do much for type safety.

The second step is to never type the return value of the loader function. We are going to do that automatically from now on. So if you have any export function loader(): SomeType, make sure you remove the SomeType from there.

Then we can start inferring the type of our loader automatically!

type LoaderType = Awaited<ReturnType<typeof loader>>;
Enter fullscreen mode Exit fullscreen mode

This essentially infers the type of the loader function.

  • Awaited extracts the type of a promise because loader can be async
  • ReturnType is pretty straightforward and returns the type returned by typeof loader

Revisiting our previous example, it would become this:

export function loader(): string {
  return "Hello world!";
}

type LoaderType = Awaited<ReturnType<typeof loader>>;

export default function SomeRemixPage() {
  const { data } = useLoaderData<LoaderType>();
  return <p>{ data }</p>;
}
Enter fullscreen mode Exit fullscreen mode

Typescript would then complain that there is no property data on type string! We can fix that by correctly refactoring our loader.

export function loader() {
  return { data: "Hello world!" };
}

type LoaderType = Awaited<ReturnType<typeof loader>>;

export default function SomeRemixPage() {
  const { data } = useLoaderData<LoaderType>();
  return <p>{ data }</p>;
}
Enter fullscreen mode Exit fullscreen mode

If you want to type the arguments of loader you can import the following from Remix internals:

import type { DataFunctionArgs } from "@remix-run/server-runtime";

export function loader(({ request }: DataFunctionArgs)) {
  // do stuff
}
Enter fullscreen mode Exit fullscreen mode

This will keep the return type untouched so you can automatically infer it.

This solution is a great help because it also takes care of conditionals! Imagine that this page is only for authenticated users:

export function loader({ request }: DataFunctionArgs) {
  if (!extractUserFromRequest(request)) return new Response(null, { status: 401 });
  return { data: "Hello world!" };
}

type LoaderType = Awaited<ReturnType<typeof loader>>;

export default function SomeRemixPage() {
  const { data } = useLoaderData<LoaderType>();
  return <p>{ data }</p>;
}
Enter fullscreen mode Exit fullscreen mode

Here, Typescript would complain that there is no data on Response when using the useLoaderData hook. This would avoid a regression here. You could quickly fix this by using throw when checking for the user session instead of return. Remember that you can throw inside a loader function to immediately stop rendering! It would also keep Typescript silent because the function only returns { data: string }.

Final notes

You can also export the types from the inferred loader functions to use wherever you want. This lets us ensure everything is nice and tidy and keep runtime errors to a minimum.

Hope this helped, let me know if you have any questions! If you also have a better solution than this one, please let me know!

Stay safe out there!

Discussion (0)