• Please complete the contact form below with details about your inquiry and I'll get back to you as soon as possible.

  • This field is for validation purposes and should be left unchanged.

Using React Router NavLink with a MUI ListItemButton + TypeScript

So, recently I needed to render a React Router NavLink component as a ListItemButton. Although the documentation on the MUI website provides a few examples on how to achieve a similar result with the Link component (from react-router-dom), I still ran into a couple issues and needed to spend a few hours to figure out what was going on.

First, I was using react-router-dom v6 and MUI v5 for this example, if you’re using different versions of these packages, you may need a different implementation. Second, you use NavLink in order to render links that will display as active whenever the URL is matching the current page URL.

import {
List,
ListItemButton,
ListItemIcon,
ListItemText,
} from '@mui/material';
import DashboardIcon from '@mui/icons-material/Dashboard';
import ListAltIcon from '@mui/icons-material/ListAlt';
import AppsOutageIcon from '@mui/icons-material/AppsOutage';
import MenuIcon from '@mui/icons-material/Menu';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import React, { ReactNode } from 'react';
import {
HashRouter as Router,
Routes,
NavLink,
NavLinkProps,
} from 'react-router-dom';

type RouterLinkProps = React.PropsWithChildren<{
  to: string,
  text: string,
  icon: ReactNode
}>

const RouterLink = (props: RouterLinkProps) => {
  type MyNavLinkProps = Omit<NavLinkProps, 'to'>;
  const MyNavLink = React.useMemo(() => React.forwardRef<HTMLAnchorElement, MyNavLinkProps>((navLinkProps, ref) => {
    const { className: previousClasses, ...rest } = navLinkProps;
    const elementClasses = previousClasses?.toString() ?? "";

    return (<NavLink
      {...rest}
      ref={ref}
      to={props.to}
      end
      className={({ isActive }) => (isActive ? elementClasses + " Mui-selected" : elementClasses)}
      />)
  }), [props.to]);

  return (
    <ListItemButton
      component={MyNavLink}
    > 
      <ListItemIcon sx={{ '.Mui-selected > &': { color: (theme) => theme.palette.primary.main } }}>
         {props.icon}
      </ListItemIcon>
      <ListItemText primary={props.text} />
    </ListItemButton>
  )
}

And then I’d use it in a Router like this:

<Router>
    <List>
       <RouterLink to="/" text="Dashboard" icon={<DashboardIcon />} />
       <RouterLink to="/jobs" text="Jobs" icon={<ListAltIcon />} />
       <RouterLink to="/logs" text="Logs" icon={<AppsOutageIcon />} />
    </List>
</Router>

A few things about the code. We memoize the MyNavLink component in order to avoid unnecessary re-renders (see this). Then, we define the MyNavLinkProps type to omit the “to” property, because first of all, “to” is a required property on the NavLinkProps, and second of all, where MyNavLink is called (from ListItemButton), the “to” parameter is not defined to be forwarded, so TypeScript will emit an error as the required prop is missing on the MyNavLink. MyNavLink does not need the “to” prop anyway because we provide it inside the MyNavLink component definition.

If something requires more explanation or you need help with the above implementation, feel free to ask in the comment section below.

7 Comments

  1. Jeff

    Your article was really helpful. But as a newcomer in Typescript and React could it be that there is a missing assignment of icon in your

    type RouterLinkProps = React.PropsWithChildren ??

    1. dragosmocrii

      You are correct, thank you for pointing that out! I’ve updated the post (added the import of ReactNode from ‘react’ and added the type prop).

    1. dragosmocrii

      Haven’t looked in detail, but does this apply to using NavLink with a MUI ListItemButton component? I remember my struggle was specifically with making these two work together.

Leave a Reply

Your email address will not be published. Required fields are marked *