user18943198
user18943198

Reputation:

The withRouter wrapper for React Router v6 correctly typed without any

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

Answers (2)

ghybs
ghybs

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!

Playground Link

Upvotes: 3

Hostek
Hostek

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

Related Questions