Reputation: 61
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:
Set handle
in my interface to an Array
, string[]
and any
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
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
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
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>
)
}
}
]
}
]);
Upvotes: 0