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.
Hi, This was very useful to my solution.
Thanks a lot.
Thanks for your time, working this article. It is very helpful.
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 ??
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).
Really helpful, new to React and this solved my problem – thanks
For someone searching for a good solution and stumbling across this, you might want to have a look at this one: https://stackoverflow.com/a/71054976/713012
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.