Type Safety in React Router
Scrolling through discussions online and reading about "Which React Framework should I use?" I always feel like React Router doesn't get enough credit. React Router is obviously mentioned but often overshadowed by the new shiny thing, TanStack Start. One of the main talking points in these discussions is type-safety. If you need a fully type-safe experience from the ground up, TanStack won't do you wrong.
Type safety isn't something that's entirely new in React Router, but it definitely got improved! If you're already in the React Router ecosystem and want to improve reliability and reduce the amount of runtime errors, this post is for you.
Route Modules
Before diving in, make sure you have setup React Router to generate types. You can go to this link to learn more.
Now you're able to use the generated types in, for example, your root route module.
// root.tsx
import type { Route } from "./+types/root";
// type-safe params from route definition
export const loader = ({ params }: Route.LoaderArgs) => {}
// type-safe "data" from loader
export const meta: MetaFunction = ({ data }: Route.MetaArgs) => {}Previously, you'd have to hint the type manually. useLoaderData() returns unknown, so you would use a typeof cast:
const data = useLoaderData<typeof loader>();With the new generated types, that's no longer needed. Your component receives loaderData directly via Route.ComponentProps, fully type-safe with return values from your loader's function.
// loaderData = type-safe yes!
export default HomeComponent({ loaderData }: Route.ComponentProps) {}Nice, but still fairly straight-forward. This works fine in the Route module itself but still means you'd have to pass on the generated types to lower-level components. Ideally we'd have something that handles this for us.
useRoute
This one flew under the radar for me since it's also still marked as unstable. The new introduced useRoute hook is also aware of your application routes. It takes a route ID, which matches the path of the route you defined in your routes.ts. The return value of useRoute contains loaderData and actionData.
useRoute is marked as unstable but I don't expect this api to change a whole lot, I'd consider this "fairly safe" to use.
import { unstable_useRoute as useRoute } from "react-router";
// passing no argument returns the current route
const current = useRoute();
// Argument = type-safe
const products = useRoute("routes/products");
// Both loaderData + actionData are type-safe
const { loaderData, actionData } = products;
// returns undefined if you're not on the checkout page.
const checkout = useRoute("routes/checkout");
if (!checkout) {
throw new Error(`Checkout accessed with useRoute on non-checkout page"`);
}For more information I'd suggest taking a look in the documentation.
href
Wouldn't it be nice to also have a way to link to pages you know exist? Let me introduce you to href().
import { href } from "react-router";
const link = href("/product/:slug", { slug: "awesome-product" })
// -> /product/awesome-product
<Link to={href("/user/:username", { username: "raphaelisthebest" })} />
// -> <a href="/user/raphaelisthebest">If you relied heavily on
navigatebefore, you might be able to refactor those to usehrefinstead.
If you rename or remove a route, Typescript will flag every broken href call. Neat!
One thing worth noting: all of the above is specific to framework mode. If you're using React Router in library mode, type generation won't be done.
I'd suggest going through the documentation on how to migrate to framework mode with React Router's vite plugin.
- https://reactrouter.com/upgrading/component-routes
- https://reactrouter.com/upgrading/router-provider
These might seem like small additions, but they add up: no more manual type-hinting, fewer runtime surprises, and links that actually break at compile time. Hope it was useful!