I'm trying to get Material-UI Breadcrumbs to work with React-Router. There is an example on the MUI site:
import * as React from 'react';
import Box from '@mui/material/Box';
import List from '@mui/material/List';
import Link, { LinkProps } from '@mui/material/Link';
import ListItem, { ListItemProps } from '@mui/material/ListItem';
import Collapse from '@mui/material/Collapse';
import ListItemText from '@mui/material/ListItemText';
import Typography from '@mui/material/Typography';
import ExpandLess from '@mui/icons-material/ExpandLess';
import ExpandMore from '@mui/icons-material/ExpandMore';
import Breadcrumbs from '@mui/material/Breadcrumbs';
import { Link as RouterLink, Route, MemoryRouter } from 'react-router-dom';
interface ListItemLinkProps extends ListItemProps {
to: string;
open?: boolean;
}
const breadcrumbNameMap: { [key: string]: string } = {
'/inbox': 'Inbox',
'/inbox/important': 'Important',
'/trash': 'Trash',
'/spam': 'Spam',
'/drafts': 'Drafts',
};
function ListItemLink(props: ListItemLinkProps) {
const { to, open, ...other } = props;
const primary = breadcrumbNameMap[to];
let icon = null;
if (open != null) {
icon = open ? <ExpandLess /> : <ExpandMore />;
}
return (
<li>
<ListItem button component={RouterLink as any} to={to} {...other}>
<ListItemText primary={primary} />
{icon}
</ListItem>
</li>
);
}
interface LinkRouterProps extends LinkProps {
to: string;
replace?: boolean;
}
const LinkRouter = (props: LinkRouterProps) => (
<Link {...props} component={RouterLink as any} />
);
export default function RouterBreadcrumbs() {
const [open, setOpen] = React.useState(true);
const handleClick = () => {
setOpen((prevOpen) => !prevOpen);
};
return (
<MemoryRouter initialEntries={['/inbox']} initialIndex={0}>
<Box sx={{ display: 'flex', flexDirection: 'column', width: 360 }}>
<Route>
{({ location }) => {
const pathnames = location.pathname.split('/').filter((x) => x);
return (
<Breadcrumbs aria-label="breadcrumb">
<LinkRouter underline="hover" color="inherit" to="/">
Home
</LinkRouter>
{pathnames.map((value, index) => {
const last = index === pathnames.length - 1;
const to = `/${pathnames.slice(0, index + 1).join('/')}`;
return last ? (
<Typography color="text.primary" key={to}>
{breadcrumbNameMap[to]}
</Typography>
) : (
<LinkRouter underline="hover" color="inherit" to={to} key={to}>
{breadcrumbNameMap[to]}
</LinkRouter>
);
})}
</Breadcrumbs>
);
}}
</Route>
<Box
sx={{
bgcolor: 'background.paper',
mt: 1,
}}
component="nav"
aria-label="mailbox folders"
>
<List>
<ListItemLink to="/inbox" open={open} onClick={handleClick} />
<Collapse component="li" in={open} timeout="auto" unmountOnExit>
<List disablePadding>
<ListItemLink sx={{ pl: 4 }} to="/inbox/important" />
</List>
</Collapse>
<ListItemLink to="/trash" />
<ListItemLink to="/spam" />
</List>
</Box>
</Box>
</MemoryRouter>
);
}
But this line:
{({ location }) => {
produces error
Type '({ location }: { location: any; }) => Element' is not assignable to type 'ReactNode'
and this line:
{pathnames.map((value, index) => {
produces error:
'value' is declared but its value is never read.ts(6133) Parameter 'value' implicitly has an 'any' type.
and:
Parameter 'index' implicitly has an 'any' type.
I've investigated similar questions, but either they have the same problem or don't work either. The problems seem to be attributed to out-of-date code.
Can anyone provide a modern solution that works?
If you are using React-Router 6 or newer then your code is missing a Routes
component wrapping any/all Route
components, and Route
components can only have React.Fragment
and other Route
components as children. When I copy your code into a running sandbox and get all the dependencies set I see the expected errors about the invalid components usage.
You should refactor the anonymous function into a standalone React component.
Create a new component, i.e. BreadcrumbsComponent
, and use the useLocation
hook to access the current location
value.
const BreadcrumbsComponent = () => {
const { pathname } = useLocation();
const pathnames = pathname.split("/").filter((segment) => segment);
return (
<Breadcrumbs aria-label="breadcrumb">
<LinkRouter underline="hover" color="inherit" to="/">
Home
</LinkRouter>
{pathnames.map((_value, index) => {
const last = index === pathnames.length - 1;
const to = `/${pathnames.slice(0, index + 1).join("/")}`;
return last ? (
<Typography color="text.primary" key={to}>
{breadcrumbNameMap[to]}
</Typography>
) : (
<LinkRouter underline="hover" color="inherit" to={to} key={to}>
{breadcrumbNameMap[to]}
</LinkRouter>
);
})}
</Breadcrumbs>
);
};
You could render BreadcrumbsComponent
on a pathless route like <Route element=<BreadcrumbsComponent />} />
but this is pointless, you can simply render BreadcrumbsComponent
directly within the MemoryRouter
.
function RouterBreadcrumbs() {
const [open, setOpen] = React.useState(true);
const handleClick = () => {
setOpen((prevOpen) => !prevOpen);
};
return (
<MemoryRouter initialEntries={["/inbox"]} initialIndex={0}>
<Box sx={{ display: "flex", flexDirection: "column", width: 360 }}>
<BreadcrumbsComponent />
<Box
sx={{
bgcolor: "background.paper",
mt: 1,
}}
component="nav"
aria-label="mailbox folders"
>
<List>
<ListItemLink to="/inbox" open={open} onClick={handleClick} />
<Collapse component="li" in={open} timeout="auto" unmountOnExit>
<List disablePadding>
<ListItemLink sx={{ pl: 4 }} to="/inbox/important" />
</List>
</Collapse>
<ListItemLink to="/trash" />
<ListItemLink to="/spam" />
</List>
</Box>
</Box>
</MemoryRouter>
);
}