Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
December 7, 2020 03:32 pm GMT

Type-Safe Usage of React Router

This is my approach to implement strongly typed routing using React Router and TypeScript. So that if I try to create a <Link> to an unknown path, tsc can warn me appropriately. Of course there are other benefits of typed routes, but let's go over what's wrong with the current implementation first.

Problem

  1. react-router takes any plain string as a path. This makes it difficult to refactor routes when it is required to rename/delete/add routes. Also typos are hard to detect.
  2. Developers need to provide types for useParams hook (i.e. useParams<{ id: string }>). It has the same issue with refactoring. Developers need to update useParams hooks whenever there's a change in URL parameter names.

Solution (Walkthrough)

I ended up implementing something I am happy with. Example source code is available on a GitHub repo. I hope this can help others who desire typed routes. This post is mostly annotation of my implementation, so if you prefer reading source code directly, check out the GitHub repo.

src/hooks/paths.tsx

The single source of truth for available paths is defined in this module. If a route needs to be modified, this PATH_SPECS can be fixed, then TypeScript compiler will raise errors where type incompatibilities are found.

const PATH_SPECS = [  {    path: '/',    params: [],  },  {    path: '/signup',    params: [],  },  {    path: '/login',    params: [],  },  {    path: '/post/:id',    params: ['id'],  },  {    path: '/calendar/:year/:month',    params: ['year', 'month'],  },] as const;
Enter fullscreen mode Exit fullscreen mode

Utility types can be derived from this readonly array of spec objects.

type PathSpec = (typeof PATH_SPECS)[number];export type Path = PathSpec['path'];// Find a path spec with the matching path.type MatchPath<T, P> = T extends { path: P } ? T : never;// Object which has matching parameter keys for a path.export type PathParams<P extends Path> = {  [X in MatchPath<PathSpec, P>['params'][number]]: string;};
Enter fullscreen mode Exit fullscreen mode

Small amount of TypeScript magic is applied here, but the end result is quite simple. Note how PathParams type behaves.

  • PathParams<'/post/:id'> is { id: string }
  • PathParams<'/calendar/:year/:month'> is { year: string, month: string }
  • PathParams<'/'> is {}

From here, a type-safe utility function is written for building URL strings.

/** * Build an url with a path and its parameters. * @param path target path. * @param params parameters. */export const buildUrl = <P extends Path>(  path: P,  params: PathParams<P>,): string => {  let ret: string = path;  // Upcast `params` to be used in string replacement.  const paramObj: { [i: string]: string } = params;  for (const spec of PATH_SPECS) {    if (spec.path === path) {      for (const key of spec.params) {        ret = ret.replace(`:${key}`, paramObj[key]);      }      break;    }  }  return ret;};
Enter fullscreen mode Exit fullscreen mode

buildUrl function can be used like this:

buildUrl(  '/post/:id',  { id: 'abcd123' },); // returns '/post/abcd123'
Enter fullscreen mode Exit fullscreen mode

buildUrl only takes a known path (from PATH_SPECS) as the first argument, therefore typo-proof. Sweet!

src/components/TypedLink

Now, let's look at TypedLink a type-safe alternative to Link.

import { Path, PathParams, buildUrl } from '../hooks/paths';import React, { ComponentType, ReactNode } from 'react';import { Link } from 'react-router-dom';type TypedLinkProps<P extends Path> = {  to: P,  params: PathParams<P>,  replace?: boolean,  component?: ComponentType,  children?: ReactNode,};/** * Type-safe version of `react-router-dom/Link`. */export const TypedLink = <P extends Path>({   to,   params,   replace,   component,   children,}: TypedLinkProps<P>) => {  return (    <Link      to={buildUrl(to, params)}      replace={replace}      component={component}    >      {children}    </Link>  );}
Enter fullscreen mode Exit fullscreen mode

TypedLink can be used like this:

<TypedLink to='/post/:id' params={{ id: 'abcd123' }} />
Enter fullscreen mode Exit fullscreen mode

The to props of TypedLink only takes a known path, just like buildUrl.

src/components/TypedRedirect.tsx

TypedRedirect is implemented in same fashion as TypedLink.

import { Path, PathParams, buildUrl } from '../hooks/paths';import React from 'react';import { Redirect } from 'react-router-dom';type TypedRedirectProps<P extends Path, Q extends Path> = {  to: P,  params: PathParams<P>,  push?: boolean,  from?: Q,};/** * Type-safe version of `react-router-dom/Redirect`. */export const TypedRedirect = <P extends Path, Q extends Path>({  to,  params,  push,  from,}: TypedRedirectProps<P, Q>) => {  return (    <Redirect      to={buildUrl(to, params)}      push={push}      from={from}    />  );};
Enter fullscreen mode Exit fullscreen mode

src/hooks/index.tsx

Instead of useParams which cannot infer the shape of params object, useTypedParams hook can be used. It can infer the type of params from path parameter.

/** * Type-safe version of `react-router-dom/useParams`. * @param path Path to match route. * @returns parameter object if route matches. `null` otherwise. */export const useTypedParams = <P extends Path>(  path: P): PathParams<P> | null => {  // `exact`, `sensitive` and `strict` options are set to true  // to ensure type safety.  const match = useRouteMatch({    path,    exact: true,    sensitive: true,    strict: true,  });  if (!match || !isParams(path, match.params)) {    return null;  }  return match.params;}
Enter fullscreen mode Exit fullscreen mode

Finally, useTypedSwitch allows type-safe <Switch> tree.

/** * A hook for defining route switch. * @param routes  * @param fallbackComponent  */export const useTypedSwitch = (  routes: ReadonlyArray<{ path: Path, component: ComponentType }>,  fallbackComponent?: ComponentType,): ComponentType => {  const Fallback = fallbackComponent;  return () => (    <Switch>      {routes.map(({ path, component: RouteComponent }, i) => (        <Route exact strict sensitive path={path}>          <RouteComponent />        </Route>      ))}      {Fallback && <Fallback />}    </Switch>  );}
Enter fullscreen mode Exit fullscreen mode

Here's how <Switch> is usually used:

// Traditional approach.const App = () => (  <BrowserRouter>    <Switch>      <Route exact path='/' component={Home} />      <Route exact path='/user/:id' component={User} />    </Switch>  </BrowserRouter>);
Enter fullscreen mode Exit fullscreen mode

The code above can be replaced with the following code.

const App = () => {  const TypedSwitch = useTypedSwitch([    { path: '/', component: Home },    { path: '/user/:id', component: User },  ]);  return (    <BrowserRouter>      <TypedSwitch />    </BrowserRouter>  );}
Enter fullscreen mode Exit fullscreen mode

Conclusion

OriginalReplaced
<Link to='/user/123' /><TypedLink to='/user/:id' params={ id: '123' } />
<Redirect to='/user/123'><TypedRedirect to='/user/:id' params={ id: '123' } />
useParams()useTypedParams('/user/:id')
<Switch>useTypedSwitch

Type-safe alternatives are slightly more verbose than the original syntax, but I believe this is better for overall integrity of a project.

  • Developers can make changes in routes without worrying about broken links (at least they don't break silently).
  • Nice autocompletion while editing code.

Original Link: https://dev.to/0916dhkim/type-safe-usage-of-react-router-5c44

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To