Giter VIP home page Giter VIP logo

typed-react-router's Introduction

Typescript-Friendly React Router Example

This example shows you how to use react-router in more type-safe way.

Problem

  1. react-router takes any plain string as a path. This makes it difficult to refactor routing 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)

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 PATHS can be fixed, then TypeScript compiler will raise errors where type incompatibilities are found.

const PATHS = [
  '/',
  '/signup',
  '/login',
  '/post/:id',
  '/calendar/:year/:month',
] as const;

Utility types can be derived from this readonly array of paths.

type ExtractRouteParams<T> = string extends T
    ? Record<string, string>
    : T extends `${infer _Start}:${infer Param}/${infer Rest}`
    ? { [k in Param | keyof ExtractRouteParams<Rest>]: string }
    : T extends `${infer _Start}:${infer Param}`
    ? { [k in Param]: string }
    : {};

export type Path = (typeof PATHS)[number];

// Object which has matching parameter keys for a path.
export type PathParams<P extends Path> = ExtractRouteParams<P>;

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 key of Object.keys(paramObj)) {
    ret = ret.replace(`:${key}`, paramObj[key]);
  }

  return ret;
};

buildUrl function can be used like this:

buildUrl(
  '/post/:id',
  { id: 'abcd123' },
); // returns '/post/abcd123'

buildUrl only takes a known path (from PATHS) 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>
  );
}

TypedLink can be used like this:

<TypedLink to='/post/:id' params={{ id: 'abcd123' }} />

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}
    />
  );
};

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;
}

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>
  );
}

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>
);

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>
  );
}

Conclusion

Original Replaced
<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.

How to Run This Example

  1. Clone this repo.
  2. yarn
  3. yarn start

typed-react-router's People

Contributors

0916dhkim avatar

Stargazers

Anna Kryva avatar dong avatar  avatar Sergei Isaev avatar Teo Boley VT avatar Ryan Valentine avatar Subin George avatar Bhavit Sharma avatar Norbert Virth avatar Lo Axhamre avatar Spase Markovski avatar Julien Goux avatar Anton Fedchenko avatar Jonathan Levi avatar DK avatar

Watchers

 avatar  avatar

typed-react-router's Issues

Implement with Modals

(cross posted on your blog post)
Love this idea, and works great but cant figure out how to implement modals with this method. Any idea how I would also implement Modal routes that would keep background page location? I don't think a second switch works because the nature how switches only matches one route.

Similar to this example but i do not contextualize whether or not info is presented in a modal or not. Its always in a modal
v5.reactrouter.com/web/example/mod...

Thanks

Type Link State

Hey, I came across your article and started implementing the code into my project. This is such awesome work, thanks so much for putting time into it.

I would like to pass an object into state in TypedLinkProps, but I'm very new to TypeScript typings. Right now, i have something like this:

<Link to={{ pathname: `${ROUTES.MENU}/${menu.id}`, state: { menu } }} >...</Link>

where 'menu' is the object in question. I would like my typed Link to recognize that this menu is required in the component that renders this route.

Could you point me in the right direction to add this? Thanks again, keep up the good work.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.