Sergio Xalambrí

The useMatches hook in Remix

If you use Remix, there's a hook that you can use called useMatches, this hook is a way to access some internal data Remix has about your app, and it let's you a lot of things! It's so awesome, that you could re-implement some of the features Remix has using it!

The hook will return an array with this interface:

interface Match {
  pathname: string;
  params: Params<string>;
  data: RouteData;
  handle: any;
}

And that array will have one match for each route that matches the current path. This usually mean you'll have at least two matches, one for the root.tsx and another for your route, if your route has nested routes then it will have more matches.

What's so special about this? You can for example use the match.data key to access the data returned by the loaders of a route. Or use the match.handle to get access to the handle export of the route.

The documentation shows you how to use the handle export to pass elements from a child route to a parent route and render a breadcrumb. To me, the most useful property is the data.

With the match.data you can access to dynamic data, not static data as the breadcrumb example shows. Let's see an example:

import { useMatches } from "remix";

export function useFlashMessages(): string[] {
  return useMatches()
    .map((match) => match.data)
    .filter((data) => Boolean(data.flash))
    .map((data) => data.flash);
}

With this hook, we could access the flash property returned by any loader to get any flash message the route wants to show, then in our root.tsx (or anywhere else actually) we could call our useFlashMessages hook to render them.

function FlashMessages() {
  let flashMessages = useFlashMessages();

  return (
    <div>
      {flashMessages.map((message) => (
        <div>{message}</div>
      ))}
    </div>
  );
}

In my Remix i18next package I use this hook to get the translations a route need.

let namespaces = useMatches()
  .flatMap((match) => (match.data?.i18n ?? {}) as Record<string, Language>)
  .reduce((messages, routeMessages) => ({ ...messages, ...routeMessages }), {});

This way, any loader can return a i18n key with all the translations it loaded for that route and the user locale, in the root I can get them and pass them to i18next to use them.

You could even use it to access a route data based on their pathname. This way you don't need to use context to pass data from a parent route to a child route, and you will be able to get a child data from a parent route.

function useParentData(pathname: string): unknown {
  let matches = useMatches();
  let parentMatch = matches.find((match) => match.pathname === pathname);
  if (!parentMatch) return null;
  return parentMatch.data;
}