user3670371
user3670371

Reputation: 61

Breadcrumbs Issue with React Router (6.16.0) and TypeScript

I've been working on learning React-Router (6.16.0) by creating a simple Breadcrumbs navigation using isMatches() as they recommend in their documentation here: https://reactrouter.com/en/main/hooks/use-matches.

Using their documentation I set up the below createBrowserRouter and Breadcrumbs component. For the life of me I can't get display the breadcrumbs. Instead I get the following error: Type 'IMatches' is not assignable to type 'ReactNode'.ts(2322) and I can't access anything in the crumb and display it using .map

Everything I've read about TypeScript tells me that the handle being returned in the isMatches() as type "unknown" or the type of the crumb is the likely cause of my issues. I've tried to set handle to be a string in the interface IMatches, an Array, etc.

Can someone please point me in the right direction on this?

index.tsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import {createBrowserRouter, RouterProvider, BrowserRouter, Link} from 'react-router-dom';
import './index.css';
import reportWebVitals from './reportWebVitals';
import 'bootstrap/dist/css/bootstrap.min.css';
import ErrorPage from './components/404/error-page';
import Profile from './components/profile/profilepage';
import Root from './root';

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorPage />,
    handle: [
      {
        crumb: <Link to="/">Dashboard</Link>
      }
    ],
    children: [
      {
        path: "profile/:userId",
        element: <Profile />,
        handle: [
          {
            crumb: <Link to="profile/:userId">Profile</Link>
          }
        ]
      },
    ],
    
  },
]);

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);

root.render(
  <React.StrictMode>
   <RouterProvider router={router} />
   
  </React.StrictMode>
);

src/components/breadcrumbs/breadcrumbs.tsx

import { match } from "assert";
import { stringify } from "querystring";
import { Params, useMatches } from "react-router-dom";

export default function Breadcrumbs() {

  interface IMatches {
    id: string;
    pathname: string;
    params: Params<string>;
    data: unknown;
    handle: unknown;     
  } 
    
  //the matches
  const initialMatches: IMatches[] = useMatches()

  console.log(initialMatches);

  const crumbs = initialMatches
    .filter((match) => Boolean(match.handle)) 

    
  return (
    <ol>
      {crumbs.map((crumb, index) => (
        <li key={index}>{crumb}</li>
      ))} 
    </ol>
  );
}

Console.log(initialMatches) gives out this object in DevTools

[
    {
        "id": "0",
        "pathname": "/",
        "params": {
            "userId": "2"
        },
        "handle": [
            {
                "crumb": {
                    "type": {},
                    "key": null,
                    "ref": null,
                    "props": {
                        "to": "/",
                        "children": "Dashboard"
                    },
                    "_owner": null,
                    "_store": {}
                }
            }
        ]
    },
    {
        "id": "0-0",
        "pathname": "/profile/2",
        "params": {
            "userId": "2"
        },
        "handle": [
            {
                "crumb": {
                    "type": {},
                    "key": null,
                    "ref": null,
                    "props": {
                        "to": "profile/:userId",
                        "children": "Profile"
                    },
                    "_owner": null,
                    "_store": {}
                }
            }
        ]
    }
]

I've tried to do some of the following:

  1. Set handle in my interface to an Array, string[] and any

  2. I've tried to create a separate interface for handle and set initialMatches.handle to it's own Array and iterate through that

I expected to be able to at least console.log(initialMatches.handle[0]) after setting the interface to equal the Array that isMatches() returns but when I do that I get an error in VS as "Property 'handle' does not exist on type 'IMatches[]'.

Upvotes: 6

Views: 3321

Answers (3)

andreivictor
andreivictor

Reputation: 8491

I've used the UIMatch interface from react-router-dom, which is defined as:

interface UIMatch<Data = unknown, Handle = unknown> {
    id: string;
    pathname: string;
    params: AgnosticRouteMatch["params"];
    data: Data;
    handle: Handle;
}

Since Handle and Data are generics, we can define a custom type for the Handle property:

type HandleType = {
  breadcrumb?: string;
};

Here is the code I used to extract breadcrumbs from the route matches:

import { useMemo } from 'react';
import { UIMatch, useMatches } from 'react-router-dom';


type HandleType = {
  breadcrumb?: string;
};

export const useBreadcrumbs = () => {
  const matches = useMatches();

  return useMemo(() => {
    return matches
      // ensures that only matches with a breadcrumb are included
      .filter((match): match is UIMatch<unknown, HandleType> => !!(match.handle as HandleType).breadcrumb)
      .map((match) => ({
        path: match.pathname,
        label: match.handle.breadcrumb,
      }));
  }, [matches]);
};

Upvotes: 0

eigachl
eigachl

Reputation: 51

Building on Drew Resse' answer To fix the typescript error on matches, I created the following types

import { Params } from "react-router-dom";

interface IMatches {
  id: string;
  pathname: string;
  params: Params<string>;
  data: unknown;
  handle: unknown;
}

type HandleType={
 crumb : (param?: string) => React.ReactNode;
}

const matches: IMatches[] = useMatches();
const crumbs = matches
.filter((match) =>
  Boolean(match.handle && (match.handle as 
  HandleType).crumb)
)
.map((match) => {
  const crumb = (match.handle as HandleType).crumb(
    match.data as string | undefined
  );
  return crumb as React.ReactNode;
});

This helped get rid of the type error. You can map through crumb and it should display the breadcrumb.

Upvotes: 5

Drew Reese
Drew Reese

Reputation: 203333

In the Breadcrumbs component the crumbs variable is still an array of IMatches.

const initialMatches: IMatches[] = useMatches()

const crumbs = initialMatches
  .filter((match) => Boolean(match.handle)); // <-- still an IMatches[]

You've only filtered the initialMatches array.

In the docs you linked to you can see they take an extra step of also mapping the filtered matches to an array of crumbs

let matches = useMatches();
let crumbs = matches
  // first get rid of any matches that don't have handle and crumb
  .filter((match) => Boolean(match.handle?.crumb))
  // now map them into an array of elements, passing the loader
  // data to each one
  .map((match) => match.handle.crumb(match.data));

Here's a simple update to your code to get the breadcrumbs working with the given data/routes. Notice that the handle is an object (not array!) with a crumb property that is a function that takes a string parameter and returns a ReactNode (e.g. JSX).

interface IMatches {
  id: string;
  pathname: string;
  params: Params<string>;
  data: unknown;
  handle: {
    crumb: (param?: string) => React.ReactNode;
  };
}

function Breadcrumbs() {
  // The matches
  const initialMatches: IMatches[] = useMatches();

  const crumbs = initialMatches
    .filter((match) => Boolean(match.handle?.crumb))
    .map((match) => match.handle.crumb(match.params.userId));

  return (
    <ol>
      {crumbs.map((crumb, index) => (
        <li key={index}>{crumb}</li>
      ))}
    </ol>
  );
}
const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorPage />,
    handle: {
      crumb: () => <Link to="/">Dashboard</Link>
    },
    children: [
      {
        path: "profile/:userId",
        element: <Profile />,
        handle: {
          crumb: (userId: string) => (
            <Link to={`/profile/${userId}`}>Profile {userId}</Link>
          )
        }
      }
    ]
  }
]);

Edit breadcrumbs-issue-with-react-router-6-16-0-and-typescript

enter image description here

Upvotes: 0

Related Questions