Reputation:
In order to not rebuild everything from class components to functional components I need to use the wrapper from the React Router Documentation:
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}
router={{ location, navigate, params }}
/>
);
}
return ComponentWithRouterProp;
}
However I need to correct types for Component
and props
in Typescript. (A quick test with any shows it works). But what are the correct types that I need to use?
This is what I tried, give Component
the Function
type but still not sure with props as I want to avoid to use any..
import { useLocation, useNavigate, useParams } from "react-router-dom";
type IComponentWithRouterProp = {
[x: string]: any;
};
function withRouter(Component: Function) {
function ComponentWithRouterProp(props: IComponentWithRouterProp) {
let location = useLocation();
let navigate = useNavigate();
let params = useParams();
return <Component {...props} router={{ location, navigate, params }} />;
}
return ComponentWithRouterProp;
}
export default withRouter;
EDIT:
So I found out that what I am looking for is React.ComponentType, which is a union of ComponentClass and ComponentFunction.
But when I use Component: React.ComponentType
then TS throws an error at my router in the return - router={{ location, navigate, params }}
Type '{ router: { location: Location; navigate: NavigateFunction; params: Readonly<Params<string>>; }; }' is not assignable to type 'IntrinsicAttributes'.
Property 'router' does not exist on type 'IntrinsicAttributes'.
So this throws a new error, how can I solve this?
Upvotes: 11
Views: 2854
Reputation: 53205
There are indeed some blogs about typing the FAQ proposed withRouter
implementation, e.g. https://whereisthemouse.com/how-to-use-withrouter-hoc-in-react-router-v6-with-typescript
But it looks like it was using a previous version of the FAQ, where location
, params
and navigate
props were directly inlined as props keys, whereas the new FAQ gathers them in a single router
dictionary. It all depends on how your class components expect to receive these props.
Since you say that ignoring type issues (typically with any
) works in your project, then it complies with this form. So let's adapt the typings:
import { useLocation, useNavigate, useParams } from "react-router-dom";
export interface WithRouterProps {
location: ReturnType<typeof useLocation>;
params: Record<string, string>;
navigate: ReturnType<typeof useNavigate>;
}
function withRouter<CProps extends { router: WithRouterProps }>(Component: React.ComponentType<CProps>) {
function ComponentWithRouterProp(props: Omit<CProps, "router">) {
let location = useLocation();
let navigate = useNavigate();
let params = useParams();
return <Component {...(props as CProps)} router={{ location, navigate, params }} />;
}
return ComponentWithRouterProp;
}
export default withRouter;
Let's try it:
class Welcome extends React.Component<{ name: string; router: WithRouterProps }> {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
// Bare component expects name and router props
<>
<Welcome name="Foo" /> {/* Error: Property 'router' is missing in type '{ name: string; }' but required in type 'Readonly<{ name: string; router: WithRouterProps; }>'.(2769)*/}
<Welcome name="Foo" router={({} as WithRouterProps)} />
</>
const WelcomeWithRouter = withRouter(Welcome);
// withRouter'ed component expects only name, router will be automatically provided
<>
<WelcomeWithRouter name="Foo" />
<WelcomeWithRouter name="Foo" router={({} as WithRouterProps)} /> {/* Error: Property 'router' does not exist on type 'IntrinsicAttributes & Omit<{ name: string; router: WithRouterProps; }, "router">'.(2322) */}
</>
Looks good!
Upvotes: 3
Reputation: 577
I think this solves your problem:
import React from "react"
import {
NavigateFunction,
Params,
useLocation,
useNavigate,
useParams,
} from "react-router-dom"
interface Router {
location: Location
navigate: NavigateFunction
params: Readonly<Params<string>>
}
export interface PropsWithRouter {
router: Router
}
export function withRouter<T extends PropsWithRouter>(
Component: React.FC<T>
): React.FC<Omit<T, "router">> {
function ComponentWithRouterProp(props: T) {
let location = useLocation()
let navigate = useNavigate()
let params = useParams()
return <Component {...props} router={{ location, navigate, params }} />
}
return ComponentWithRouterProp as React.FC<Omit<T, "router">>
}
And then when you are creating components do something like this:
import React from "react"
import { PropsWithRouter, withRouter } from "./withRouter"
interface TestProps extends PropsWithRouter {
test: number
}
const Test: React.FC<TestProps> = ({ router, test }) => {
return <div>Hello World!</div>
}
export default withRouter(Test)
Upvotes: 6