Home > database >  React-router-dom and TypeScript
React-router-dom and TypeScript

Time:10-03

I'm still new to typeScript, so here is what I'm trying to do:

I have one functional component from which I try to navigate to another one using History.push, so the pattern I try to use is something like this:

history.push({
to: "some URL",
state: {// some state}
})

so it doesn't seems to be working, so here is my parent component (the one I'm trying to navigate from)

import React from 'react';
import { VideoDataType } from '../Shared/VideoDataType/VideoDataType';
import { Options } from 'react-youtube';
import moment from 'moment';

import { useHistory, withRouter, RouteComponentProps } from 'react-router-dom';

import './VideoCard.styles.css';

interface ChildComponentProps extends RouteComponentProps<any> {
  /* other props for ChildComponent */

  video: VideoDataType;
  opts: Options;
}
const VideoCard = ({
  video,
  opts,
}: ChildComponentProps): React.ReactElement => {
  const history = useHistory();
  const handleVideoSelect = (): void => {
    history.push({
      to:`/video/${video.id.videoId}`,
      state: {video, opts}
    });
  };
  return (
    <div
      className='videoCArd__wrapper fix_margins'
      onClick={() => handleVideoSelect()}
    >
      <img src={video.snippet.thumbnails.medium.url} alt='avatar' />
      <div className='videoCard__info'>
        <div className='videoCard__text'>
          <h4 className='videoCard__title'> {video.snippet.title}</h4>
          <div className='videoCard__headline'>
            <p> {video.snippet.channelTitle} &#9679; </p>
            <p> {moment(video.snippet.publishedAt).fromNow()} </p>
          </div>
          <div className='videoCard__description'>
            {video.snippet.description}
          </div>
        </div>
      </div>
    </div>
  );
};

export default withRouter(VideoCard);

and here is my targeted component (the now I'm navigating to)

import React from 'react';
import { Options } from 'react-youtube';
import { VideoDataType } from '../Shared/VideoDataType/VideoDataType';
import { RouteComponentProps, withRouter } from 'react-router-dom';

interface ChildComponentProps extends RouteComponentProps<any> {
  /* other props for ChildComponent */
}

const SingleVideoDisplayerPage =
  (): React.ReactElement<ChildComponentProps> => {
    return (
      <div>
        <h6> jksdgfdsghjfkd </h6>
      </div>
    );
  };

export default withRouter(SingleVideoDisplayerPage);

and here is the issue:

No overload matches this call.
  Overload 1 of 2, '(path: string, state?: unknown): void', gave the following error.
    Argument of type '{ to: string; state: { video: VideoDataType; opts: Options; }; }' is not assignable to parameter of type 'string'.
  Overload 2 of 2, '(location: LocationDescriptor<unknown>): void', gave the following error.
    Argument of type '{ to: string; state: { video: VideoDataType; opts: Options; }; }' is not assignable to parameter of type 'LocationDescriptor<unknown>'.
      Object literal may only specify known properties, and 'to' does not exist in type 'LocationDescriptorObject<unknown>'.  TS2769

    20 |   const history = useHistory();
    21 |   const handleVideoSelect = (): void => {
  > 22 |     history.push({
       |     ^
    23 |       to: `/video/${video.id.videoId}`,
    24 |       state: { video, opts },
    25 |     });


CodePudding user response:

The problem lies in the VideoCard component. the typing should be like this:

// This is a random typing I did to showcase the example.
type RouteState = {
  video: {
    id: number;
  };
  opts: {
    required: boolean;
  };
};

// This will be the type you will pass to the VideoCard component
// It receives the state we just did as the Third generic
type IVideoProps = RouteComponentProps<{}, StaticContext, RouteState>

After that in order to use the higher order component in your component you need to use the correct types, so basically that HOC is putting as a prop every react router property, that is why the IVideoProps has the StaticContext and the RouteState so you will call it like this:

const VideoCard = ({ location: { state } }: IVideoProps) => {
  // This condition needs to be here because the state can be undefined if you are not accessing the component via link or history.
  if (!state) return null;

  const { video } = state;

  return (
    <div>
      <h2>Video</h2>
      {video.id}
    </div>
  );
};

export default withRouter(VideoCard);

The problem you were having is that if you extend the RouteComponentProps you will need to pass those extra props to that component, separately, the video and opts, and they will not be linked to the router at all, it will be just passing the props as you would do normally, just that it does not make a lot of sense doing it, since you would have to use some sort of HOC to retrieve the values, since you can't pass them using a Link or react history.

Last but not least I will suggest to use useLocation hook to make the typing easier.

import { useLocation } from "react-router";
import { RouteState } from "./Router";
const VideoCard = () => {
  const { state } = useLocation<RouteState>();
  if (!state) return null;

  const { video } = state;

  return (
    <div>
      <h2>Video</h2>
      {video?.id}
    </div>
  );
};

export default VideoCard;

You can see that using the hook only requires to pass the State type as a generic argument const { state } = useLocation<RouteState>(); so it is even easier.

In that way the typing will be done correctly, feel free to check the sandbox in case you have any questions: https://codesandbox.io/s/gifted-mclaren-opp0p

  • Related