Remix Route Helpers: A Better Way to Use Parent Data
Before we get going let's do a little TL;DR on why this matters.
Remix leverages nested routing to only fetch data for the part of the route that changes. This means if you fetch a resource in the parent route and navigate between the child routes you will only fetch the resource once.
From the docs:
Remix also has some built in optimizations for client side navigation. It knows which layouts will persist between the two URLs, so it only fetches the data for the ones that are changing. A full document request would require all data to be fetched on the server, wasting resources on your back end and slowing down your app.
So, at some point while you're building a Remix application you're going to run into this question:
How do I get parent data in a child route?
The kicker? Remix provides a tool for fetching the data from other loaders for free.
useMatches
A utility that can be used to loop through all routes called during load and access the data that was returned in each routes loader.
Let's take a look at an app where you can write and manage a series of web novels.
You have a route novels/$novelSlug
where you have a shared layout for all of your novel routes. In the loader for that route you fetch the novel from your database so that all of the child routes do not have to refetch that data when you navigate between them.
// novels/$novelSlug export function loader({ params }: LoaderFunctionArgs) { const novel = fetchNovel(params.novelSlug); // Some error handling return json({ novel: fetchNovel }) }
Now let's say you have a child route where you want to show a summary of the novel. You can leverage useMatches
to get that data from the parent loader.
// novels/$novelSlug/summary export default function SummaryPage() { const novelData = useMatches().find((m) => m.id === "/novels/$novelId")?.data as { novel: Novel } invariant(novelData, "Could not find parent data needed to render page.") const { novel } = novelData ... }
Dope, just like that we have the data from the parent without the extra performance cost of fetching between page loads. Buuuut let's picture doing that on every child page.
Then what happens if we decide to rebrand from “novels” to “stories” and all of our routes change?
Or since we are using type casting, what happens if we change what data is returned on the parent?
As helpful as useMatches
is calling it directly in child routes can lead to more duplication and easy to miss bugs.
Reliably fetch parent data with route helpers
To fix our concerns with fragile implementation at the call site, we are going to define all types and fetching in the parent route itself using a few easy helpers.
ROUTE_ID
This provides an easy reference for your route id so that that if it changes all references to this ID are updated in one location.
const ROUTE_ID = 'routes/novels/$novelSlug' export { ROUTE_ID as novelRouteId } export type NovelMetaRecord = { [ROUTE_ID]: typeof loader }
This is often used in meta functions where you want to add dynamic parent data. For instance, including the novel title in child routes.
export const meta: MetaFunction= ({ parentsData }) => ({ title: `Summary | ${parentsData.novel.title}` })
useNovelLoaderData
Okay, this is the one. It probably looks familiar because it is basically the same method used in the example above but there are three things that make this more reliable and maintainable.
export function useNovelLoaderData(): SerializeFrom{ const matches = useMatches() const match = useMemo(() => matches.find(m => m.id === ROUTE_ID), [matches]) invariant(match, 'Unable to find cluster layout data') return match.data as SerializeFrom ; }
It lives where the data is
Instead of this process being written out in every child route we write it once. in the parent file that loads the data. We leverage the ROUTE_ID
discussed above so that route changes are not a concern, and if this route is removed we get errors during development where this data is trying to be fetched.
Not to mention if you use this VS Code plugin you get reference counts on the function itself so that you can quickly see how many routes use it.
It memoizes the result
All call sites are expecting the exact same data from the exact same route. Because useMatches
returns an array of all routes and the data attached useMemo
is a great tool provided by React to say “if we are passing you the same route just give us the same data you did last time”.
Is useMemo
going to be a giant perf gain with the matches array being as small as it is? Probably not, but will provide some benefit for no risk and that sounds pretty good to me.
Types are included at call site
This is a nice little extra benefit of having it collocated with the loader it is referencing. We do not have to create and export a type for casting data all over our routes. We get to directly reference the loader type in the helper and call sites get that for free.
useNovelSlugParam
I'm just going to throw this one in there too. It is a helper that is occasionally useful that achieves the same goal as the loader helper but when you only need the param and not the entire object.
export function useNovelSlugParam(): string { const { novelSlug } = useParams<'novelSlug'>() invariant(novelSlug, 'No novel slug in URL params') return novelSlug }
Do you have any useful helpers or tips?
That's all there is to it. If you have any interesting helpers like this hit me up on Twitter and share!
I hope you enjoyed
There is a lot more coming...
If you want to get updates when I publish new guides, demos, and more just put your email in below.