DEV Community

Cover image for My solution for React Router type safety
Leonid Fenko
Leonid Fenko

Posted on

My solution for React Router type safety

I like my code fully typed. Unfortunately, type safety for route parameters has never been a strong suit of React Router.

If all you need is to build a path with parameters, the use of generatePath will give you some typing, albeit not perfect. However, there is no built-in way to build a query or a hash, let alone type them, and there is no type safety for a route state either.

Things get even worse when it comes to parsing. There is no built-in way to parse a query or a hash, and all typing is almost exclusively done by casting, which is error-prone.

There are some libraries for providing this type safety, but they are more or less incomplete and restrictive. The best I've seen so far is typesafe-routes, but it offers no type safety for a route state and a hash, and it puts restrictions on what paths can be used. For instance, it doesn't support custom regexps for parameters.

The solution

Enter react-router-typesafe-routes. It tries to be as comprehensive, extensible, and non-restrictive as possible.

Route definition

import { route, path, query, hash } from "react-router-typesafe-routes";
import { state } from "./path/to/state";

const someRoute = route(path("/path/:id"), query(), hash(), state());
Enter fullscreen mode Exit fullscreen mode

There are several helpers for processing different parts of a route:

  • path uses generatePath to build a path string, making it possible to use any path template that's compatible with React Router. When it comes to parsing, React Router extracts params from a path string, and path performs various checks on these params to ensure that they belong to the specified path template. By default, path infers types of path params from a path template in the same way as generatePath does.
  • query uses (configurable!) query-string to build and parse a query string. By default, query uses the same types for query params as query-string does.
  • hash just takes care of the # symbol while building and parsing a hash string. By default, hash uses the string type for a hash.
  • state is an ad-hoc helper written by the user. The library doesn't provide a generic helper for route state processing.

As expected, the types can be improved:

import { route, path, query, hash, param } from "react-router-typesafe-routes";

const someRoute = route(
    path("/path/:id(\\d+)?", { id: param.number.optional }),
    query({ search: param.string.optional("") }), // Use "" as a fallback
    hash("about", "subscribe")
);
Enter fullscreen mode Exit fullscreen mode

The param helper defines a set of transformers that transform values while building and parsing. The built-in transformers are param.string, param.number, param.boolean, param.null, param.date, param.oneOf(), and param.arrayOf().

The optional modifier means that the corresponding value can be undefined. An unsuccessful parsing of an optional parameter will also result in undefined. It's possible to specify a fallback value that will be returned instead of undefined. This should be particularly useful for query params.

Note that query params are optional by their nature. React Router doesn't consider the query part on route matching, and the app shouldn't break in case of manual URL changes.

The transformers are very permissive. It's possible to (natively!) store arrays in a query and even in a path, and it's possible to write custom transformers for storing any serializable data.

Route usage

Use Route components as usual:

import { Route } from "react-router";
import { someRoute } from "./path/to/routes";

<Route path={someRoute.path} />;
Enter fullscreen mode Exit fullscreen mode

Use Link components as usual:

import { Link } from "react-router-dom";
import { someRoute } from "./path/to/routes";

// Everything is fully typed!
<Link to={someRoute.build({ id: 1 }, { search: "strawberries" }, "about")} />;
<Link to={someRoute.buildLocation({ state: "private" }, { id: 1 }, { search: "strawberries" }, "about")} />;
Enter fullscreen mode Exit fullscreen mode

Parse params with usual hooks:

import { useParams, useLocation } from "react-router";
import { someRoute } from "./path/to/routes";

// You can use useRouteMatch() instead of useParams()
const { path, query, hash, state } = someRoute.parse(useParams(), useLocation());
Enter fullscreen mode Exit fullscreen mode

Parse only what you need:

import { useParams, useLocation } from "react-router";
import { someRoute } from "./path/to/routes";

// Again, you can also use useRouteMatch()
const path = someRoute.parsePath(useParams());
const query = someRoute.parseQuery(useLocation());
const hash = someRoute.parseHash(useLocation());
const state = someRoute.parseState(useLocation());
Enter fullscreen mode Exit fullscreen mode

Notes

A more detailed description is available at the project page. The library requires battle-testing and has yet to reach version 1.0.0.

Top comments (0)