How to use withRouter HOC in React Router v6 with Typescript

How to use withRouter HOC in React Router v6 with Typescript

Iva Kop's photo
Iva Kop
·Jun 18, 2022·

3 min read

Subscribe to my newsletter and never miss my upcoming articles

Play this article

Table of contents

  • Creating our own withRouter HOC
  • Adding Typescript types to withRouter
  • Testing withRouter with Jest

React Router v6 has been around for a while now. React developers who are still using older versions of the library are probably aching to upgrade. And for a good reason. Among other improvements, React Router v6 has been built from the ground up using React hooks, making it more compatible with future versions of React and reducing its bundle size significantly. But, as with every major upgrade, inevitably there are breaking changes.

One of the major blockers when it comes to switching to React Router v6 is that the new version fully embraces React hooks - meaning that the withRouter HOC (higher-order component) is no longer part of the library.

As a consequence, if withRouter is used extensively in our application, especially with class components that cannot be immediately switched to React hooks, we are in for a major refactor.

Thankfully, there's an easy way around it. Let's create our own withRouter HOC!

Creating our own withRouter HOC

As pointed out in React Router's FAQ page - creating our own wrapper to replicate withRouter is trivial:

import {
  useLocation,
  useNavigate,
  useParams,
} from "react-router-dom";

function withRouter(Component) {
  function ComponentWithRouterProp(props) {
    let location = useLocation();
    let navigate = useNavigate();
    let params = useParams();
    return (
      <Component
        {...props}
        location={location}
        params={params}
        navigate={navigate}
      />
    );
  }

  return ComponentWithRouterProp;
}

After, all we need to do is change the withRouter imports from react-router to the location of our new component, and we are done! Everything is working as before.

Now we can gradually transition away from withRouter without having to refactor everything at once.

But wait, what if we are using Typescript? Surely, we want to preserve type safety with our new HOC.

Adding Typescript types to withRouter

Creating types for a React HOC can be tricky. Let's see how we can go about it in this case:

import React from 'react';
import {
  useLocation,
  useNavigate,
  useParams,
  useSearchParams,
} from 'react-router-dom';

/** @deprecated Use `React Router hooks` instead */
export interface WithRouterProps {
  location: ReturnType<typeof useLocation>;
  params: Record<string, string>;
  navigate: ReturnType<typeof useNavigate>;
}

/** @deprecated Use `React Router hooks` instead */
export const withRouter = <T extends WithRouterProps>(
  Component: React.ComponentType<T>
) => {
  return (props: Omit<T, keyof WithRouterProps>) => {
    const location = useLocation();
    const params = useParams();
    const navigate = useNavigate();

    return (
      <Component
        {...(props as T)}
        location={location}
        params={params}
        navigate={navigate}
      />
    );
  };
};

As you can see above, to type the props coming from the react-router-dom hooks, we can use the types from the library itself. We are also using a Typescript generic for the prop types of the component that is being wrapped in withRouter. That's it!

We've also added a deprecation warnings for both withRouter and withRouterProps to document our preference for react-router-dom hooks.

Testing withRouter with Jest

Finally, in order to unit test components that are using withRouter with Jest, we might need to mock the implementation:

jest.mock('../withRouter', () => {
  return {
    withRouter: (Component: React.ComponentType<WithRouterProps>) => {
      return (props: WithRouterProps) => {
        return <Component {...props} params={{ id: '1' }} />;
      };
    },
  };
});

Alternatively, we can also create a global mock for it the appropriate __mocks__ folder:

import React from 'react';

export const withRouter = (Component: React.ComponentType<WithRouterProps>) => {
  return (props: : WithRouterProps) => {
    return <Component {...props} location={{ pathname: 'pathname' }} />;
  };
};

And reuse the mock in our tests by including it in the test file:

jest.mock('../withRouter')

For more information on how to migrate to React Router v6, check out the documentation here.

If you found this article useful, continue reading my blog and follow me on Twitter for more tech content.

Happy coding! ✨

Did you find this article valuable?

Support Iva Kop by becoming a sponsor. Any amount is appreciated!

See recent sponsors Learn more about Hashnode Sponsors
 
Share this