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! โจ