reactjstypescriptmaterial-uireact-routerbreadcrumbs

MUI Breadcrumb Integration with react-router


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?


Solution

  • 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>
      );
    }