Reese McLean

Saturday, January 7, 2023

Remix RouteData Type

In Remix, a child route can contribute content to a layout route via a handle named export object. Through this we can have the child route return a chunk of UI based on it’s data loader. In the example below, we might have a child route contribute a link that a layout route might use to show breadcrumbs.

// Child Route File
export const loader = async ({ request, params }: LoaderArgs) => {
  return { username: "Reese", userId: 1 };
};

export default function ChildRoute() {
  return <p>My Content</p>;
}

export const handle = {
  breadcrumb: (routeMatch: RouteMatch) => {
    const { username, userId } = routeMatch.data;

    return <Link to={`/users/${userId}`}>{username}</Link>;
  },
};

// Layout Route File
export default function Layout() {
  const matches = useMatches();

  const breadcrumbs = matches
    .filter((match) => match.handle && match.handle.breadcrumb)
    .map((match, index) => {
      return <li key={index}>{match.handle!.navMenuItem(match)}</li>;
    });

  return <ul>{breadcrumbs}</ul>;
}

Typesafety

In the example above we have a typesafety issue - anything we access on routeMatch.data will be an any type even though we know the it will match the result of our loader.

Here’s how we can do this with some Typescript utilities… we’ll do this in a few steps to understand why each utility is needed since our loader is a function that returns a Promise.

// The type here is a RouteData
// This is essentially an any type
const attemptOne = routeMatch.data;

// The type here is a function
// What we need is the type of the result of that function
const attemptTwo = routeMatch.data as typeof loader;

// The type here is a promise
// The loader returns a Promise but the data we get in routeMatch is the resolved data from that Promise.
const attemptThree = routeMatch.data as ReturnType<typeof loader>;

// The type here is the result of the promise that is returned by the loader function.
const attemptFour = routeMatch.data as Awaited<ReturnType<typeof loader>>;

Summary

As we see with using RouteData, there are times where we want to get the type of the resolved result of a Remix loader function. There are two layers to get through to get the result.

  1. The loader is a function so we use the ReturnType<> Typescript utility to get the result type of the function
  2. The function returns a promise so we use the Awaited<> Typescript utility to get the result type of the Promise.

Putting it all together we end up with:

const data = routeMatch.data as Awaited<ReturnType<typeof loader>>;

Now our data has the proper type. As our loader changes we can be sure that anything relying on its data will fail to compile if we try to access parts of that data that are no longer available.