How to use withRouter HOC in React Router v6 with Typescript

How to use withRouter HOC in React Router v6 with Typescript

ยท

3 min read

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 Where is the mouse? by becoming a sponsor. Any amount is appreciated!