react-router-dom v6 NavLink and MUI ListItem not working with className
Asked Answered
S

7

8

I have a problem using MUI with react-router-dom v6.

import ListItem from '@mui/material/ListItem';
import { NavLink } from 'react-router-dom';
<List key={index}>
  <ListItem
     component={NavLink}
     sx={{
       color: '#8C8C8C',
     }}
     to={'/home'}
     className={({ isActive }) => (isActive ? classes.activeLink : undefined)
     >
     <ListItemIcon></ListItemIcon>
   <ListItemText primary='Home'/>
 </ListItem>
</List>

className not working and show error:

No overload matches this call.
  The last overload gave the following error.
    Type '({ isActive }: { isActive: any; }) => boolean' is not assignable to type 'string'
The expected type comes from property 'className' which is declared here on type 'IntrinsicAttributes & { button?: false | undefined; } & ListItemBaseProps & { components?: { Root?: ElementType<any> | undefined; } | undefined; componentsProps?: { ...; } | undefined; } & CommonProps & Omit<...>'
Softspoken answered 4/3, 2022 at 11:2 Comment(0)
S
2

Thanks all, and here is my solution

import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import { Theme } from '@mui/material/styles';
import { createStyles, makeStyles } from '@mui/styles';
import React from 'react';
import { NavLink } from 'react-router-dom';

const MyNavLink = React.forwardRef<any, any>((props, ref) => (
  <NavLink
    ref={ref}
    to={props.to}
    className={({ isActive }) => `${props.className} ${isActive ? props.activeClassName : ''}`}
  >
    {props.children}
  </NavLink>
));

const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    activeLink: {
      backgroundColor: '#19ABC0',
      color: '#FFFFFF',
      borderRadius: 8,
      '& .MuiSvgIcon-root': {
        color: '#FFFFFF',
        stroke: '#FFFFFF',
        fill: '#19ABC0',
      },
    },
  })
);

function Sidebar() {
  const classes = useStyles();

  return (
    <>
      <List>
        <ListItem
          component={MyNavLink}
          sx={{
            color: '#8C8C8C',
          }}
          to={'/home'}
          activeClassName={classes.activeLink}
        >
          <ListItemIcon sx={{ stroke: '#8C8C8C', fill: '#FFFFFF' }}></ListItemIcon>
          <ListItemText primary={'Home'} />
        </ListItem>
      </List>
      <List>
        <ListItem
          component={MyNavLink}
          sx={{
            color: '#8C8C8C',
          }}
          to={'/dashboard'}
          activeClassName={classes.activeLink}
        >
          <ListItemIcon sx={{ stroke: '#8C8C8C', fill: '#FFFFFF' }}></ListItemIcon>
          <ListItemText primary={'Dashboard'} />
        </ListItem>
      </List>
    </>
  );
}

className={({ isActive }) => `${props.className} ${isActive ? props.activeClassName : ''}`}

To use MUI's className and add NavLink's active class.

Softspoken answered 5/3, 2022 at 1:3 Comment(0)
T
10

Issue

The issue is that the className prop is for the ListItem, not the component you've specified to be rendered. It's not an extraneous prop and isn't passed along to the NavLink component.

Solution

The solution appears to be to create a custom navlink component with the dynamic className prop enclosed. Pass your dynamic className function on a different prop, say activeClassName, and pass this to the NavLink's className prop internally.

Example:

import { NavLink as NavLinkBase } from 'react-router-dom'; 

const NavLink = React.forwardRef((props, ref) => (
  <NavLinkBase
    ref={ref}
    {...props}
    className={props.activeClassName}
  />
));

...

import ListItem from '@mui/material/ListItem';
import { NavLink } from '../path/to/NavLink';

...

<List key={index}>
  <ListItem
    component={NavLink}
    activeClassName={({ isActive }) =>
      isActive ? classes.activeLink : undefined
    }
    sx={{ color: '#8C8C8C' }}
    to="/home"
  >
    <ListItemIcon></ListItemIcon>
    <ListItemText primary='Home' />
  </ListItem>
</List>

Edit react-router-dom-v6-navlink-and-mui-listitem-not-working-with-classname

Therron answered 4/3, 2022 at 16:54 Comment(0)
L
4

The className property in a ListItem can only accept a string However on a NavLink, you can define a function that has its state (active or inactive)

You can create a CustomNavLink component that gets the properties from the ListItemButton (using forwardRef) and also have different styles based on its state

If you want to preserve the styling of mui's ListItemButton make sure you add its appropriate mui css classname to the className property of the NavLink. You can then add the different styling classes for whether it's active or not.

const CustomNavLink = forwardRef((props, ref) => (
  <NavLink
    ref={ref}
    {...props}
    className={({ isActive }) => (isActive ? props.className + ' Mui-selected' : props.className)}
    end
  />
));

Make sure you use the CustomNavLink as the component for ListItemButton and add the to property which will be forwarded to the NavLink

<List>
  <ListItem>
    <ListItemButton
      component={CustomNavLink}
      to="/"
    >
      <ListItemIcon><ListIcon /></ListItemIcon>
      <ListItemText primary='Home' />
    </ListItemButton>
  </ListItem>
</List>
Ladybug answered 6/5, 2022 at 15:26 Comment(1)
This solved my related problem of how to render a selected Drawer item as "selected" based on the behavior of the NavLink component from ReactRouter. Although this remains magical to me, it is at least magic that works -- after I've spent a long time thrashing at alternatives. I've upvoted this, and at least for me it is the answer to my own question that brought me here. Kudo's to @Ladybug for posting this.Hillery
H
3

It's bothering me this couple of days.. and what I found to be the least complicated solution is to add !important; to the CSS of .active

  <Button component={NavLink} to="/home">
  Home
  </Button>
  .active {
    color: #F07DEA !important;
  }
Hyaloid answered 3/9, 2022 at 8:1 Comment(0)
S
2

Thanks all, and here is my solution

import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import { Theme } from '@mui/material/styles';
import { createStyles, makeStyles } from '@mui/styles';
import React from 'react';
import { NavLink } from 'react-router-dom';

const MyNavLink = React.forwardRef<any, any>((props, ref) => (
  <NavLink
    ref={ref}
    to={props.to}
    className={({ isActive }) => `${props.className} ${isActive ? props.activeClassName : ''}`}
  >
    {props.children}
  </NavLink>
));

const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    activeLink: {
      backgroundColor: '#19ABC0',
      color: '#FFFFFF',
      borderRadius: 8,
      '& .MuiSvgIcon-root': {
        color: '#FFFFFF',
        stroke: '#FFFFFF',
        fill: '#19ABC0',
      },
    },
  })
);

function Sidebar() {
  const classes = useStyles();

  return (
    <>
      <List>
        <ListItem
          component={MyNavLink}
          sx={{
            color: '#8C8C8C',
          }}
          to={'/home'}
          activeClassName={classes.activeLink}
        >
          <ListItemIcon sx={{ stroke: '#8C8C8C', fill: '#FFFFFF' }}></ListItemIcon>
          <ListItemText primary={'Home'} />
        </ListItem>
      </List>
      <List>
        <ListItem
          component={MyNavLink}
          sx={{
            color: '#8C8C8C',
          }}
          to={'/dashboard'}
          activeClassName={classes.activeLink}
        >
          <ListItemIcon sx={{ stroke: '#8C8C8C', fill: '#FFFFFF' }}></ListItemIcon>
          <ListItemText primary={'Dashboard'} />
        </ListItem>
      </List>
    </>
  );
}

className={({ isActive }) => `${props.className} ${isActive ? props.activeClassName : ''}`}

To use MUI's className and add NavLink's active class.

Softspoken answered 5/3, 2022 at 1:3 Comment(0)
A
1

Just solved using the Children as function option

<NavLink to={props.to} style={{ 'color': 'unset', 'textDecoration': 'unset'}}>
  {({isActive}) => (
    <ListItem disablePadding>
      <ListItemButton selected={isActive}>
        <ListItemIcon>
          <InboxIcon />
        </ListItemIcon>
        <ListItemText primary={props.text} />
      </ListItemButton>
    </ListItem>    
  )}
</NavLink>
Approximate answered 30/4 at 22:11 Comment(0)
C
0

Solution using useMatch hook from react

import Link from '@mui/material/Link';
import { Link as RouterLink, useMatch } from 'react-router-dom';

interface TopNavLinkProps {
    to: string;
    children?: React.ReactNode;
}

const TopNavLink = ({ to = '/', children }: TopNavLinkProps) => {
    // "/*" at the end is to make sure that routes like 
   //'/about/company' and '/about/team' will still match the top nav item 
    // '/about'
  const isActive = useMatch({ path: `${to}/*`, end: true }); 
    
    return (
        <Link
            variant="button"
            to={to}
            sx={{ textDecoration: isActive ? 'underline' : 'none' }}
            component={RouterLink}
        >
            {children}
        </Link>
    );
};

After that use TopNavLink component and it will be route-aware

<nav>
 <TopNavLink to="/">Home</TopNavLink>
 <TopNavLink to="/about">About</TopNavLink>
</nav>
                   
Conductivity answered 8/11, 2023 at 20:22 Comment(0)
D
0

Expanding Eduardo Lopez's answer, although the Mui docs describe several strategies I think the react-router-dom's way is more flexible for this scenario.

Taking into account that if you need a link you must render an anchor tag <a> I found that the best way to integrate a <NavLink/> with a Mui element and avoid typescript errors is this

<ListItem key={text} disablePadding sx={{ display: 'block' }}>
  <NavLink to="/example">
    {({ isActive }) => (
      <ListItemButton
        component={'div'}
        sx={{
          minHeight: 48,
          justifyContent: open ? 'initial' : 'center',
          px: 2.5,
        }}
        // We can use isActive to trigger MUI's active styles without
        //override any class (You can do so if you want)
        selected={isActive}
        role={undefined}
      >
        <ListItemIcon .../>
        <ListItemText .../>
      </ListItemButton>
    )}
  </NavLink>
</ListItem>

There is a catch, now the NavLink renders an <a> and we cannot change that. So we must set the children of <NavLink> as 'div' because we cannot render an <a> or <button> inside another <a> element. Rendering a <div> inside an <a> is valid HTML5 so you are ok. Always check the HTML you render, for example omitting role={undefined} in the <ListItemButton> can lead to a <div role="button"> inside a <a> tag that is incorrect.

Here is an example of the rendered HTML

<li class="MuiListItem-root MuiListItem-gutters css-1uhycto-MuiListItem-root">
  <a class="" href="/">
    <div class="MuiButtonBase-root ..." tabindex="0" >
      <div class="MuiListItemIcon-root ...">
        <svg .../>
      </div>
      <div class="MuiListItemText-root ...">
        <span class="MuiTypography-root ...">Inbox</span>
      </div>
      <span class="MuiTouchRipple-root ..."></span>
    </div>
  </a>
</li>

With this approach, you can safely use all the functionalities from both react-router-dom's <NavLink> and MUI's components even across different MUI versions. You can also apply the same strategy with react-router-dom's <Link>.

Source: my comment on github

Duplet answered 22/9 at 22:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.