Skip Main Navigation
Ben IlegboduBen Ilegbodu

Wrapping next/link to use with a custom UI Link component

How to create a custom Link component that combines the Next's Link component with one from a UI component library like MUI

Sunday, November 07, 2021 · 4 min read

Next.js has a pretty snazzy file-system based router that is built on the concept of pages. The router allows us to do client-side route transitions between pages similar to a single-page application (aka SPA). Next exports a React component called Link to automatically handle these client-side route transitions.

import Link from 'next/link'

const Home = () => {
  return (
    <ul>
      <li>
        <Link href="/">
          <a>Home</a>
        </Link>
      </li>
      <li>
        <Link href="/about">
          <a>About Us</a>
        </Link>
      </li>
      <li>
        <Link href="/blog/hello-world">
          <a>Blog Post</a>
        </Link>
      </li>
    </ul>
  )
}

export default Home

But the Next <Link> renders a vanilla <a> tag with no styling, which pretty much no one is going to want. We may throw a className on the <a> for styling, but more than likely we have a custom <Link> component of our own that handles the styling. I use the fantastic MUI React component library for my personal projects, and it has its own Link component. So with MUI, the example becomes:

import NextLink from 'next/link'
import { Link as MuiLink } from '@mui/material'

const Home = () => {
  return (
    <ul>
      <li>
        <NextLink href="/" passHref>
          <MuiLink>Home</MuiLink>
        </NextLink>
      </li>
      <li>
        <NextLink href="/about" passHref>
          <MuiLink>About Us</MuiLink>
        </NextLink>
      </li>
      <li>
        <NextLink href="/blog/hello-world" passHref>
          <MuiLink>Blog Post</MuiLink>
        </NextLink>
      </li>
    </ul>
  )
}

export default Home

Notice that we had to add the passHref prop to the <NextLink> so that the href is passed down to the <MuiLink>. Otherwise we'd have to duplicate the href prop on both the <NextLink> and the <MuiLink> components.

It's important to note that if your component library's Link component is a function component (more than likely it is given Hooks), it must wrap the component in React.forwardRef.

But having to do this double <Link> dance every time we want to render a styled link gets annoying, especially if we're passing more props to the <NextLink> and <MuiLink>. So what I typically do in my Next apps is create a lightweight custom Link component that wraps both next/link and MUI Link.

import { forwardRef } from 'react'
import NextLink from 'next/link'
import { Link as MuiLink } from '@mui/material'

/**
 * A convenience component that wraps the MUI `Link` component that provides
 * our look & feel with Next's router `Link`
 *
 * @see https://next.js.org/docs/api-reference/next/link
 */
const Link = forwardRef(function Link(
  { href, prefetch, replace, scroll, shallow, locale, ...muiProps },
  ref,
) {
  return (
    <NextLink
      href={href}
      replace={replace}
      scroll={scroll}
      shallow={shallow}
      locale={locale}
      passHref
    >
      <MuiLink ref={ref} {...muiProps} />
    </NextLink>
  )
})

export default Link

The component isn't terribly complex. It takes in the props and passes the appropriate ones to the underlying <NextLink> versus the <MuiLink>. Because it's a function component, it also uses forwardRef so that it can still support refs like the underlying <MuiLink>.

I use a function declaration (function Link) instead of my typical arrow function so that the component definition within forwardRef still has a component name (Link). It helps with debugging in the DevTools so that it'll say ForwardRef(Link) instead of just ForwardRef.

But since I develop in React with TypeScript, my Link component actually looks like this:

import { forwardRef } from 'react'
import NextLink, { LinkProps as NextLinkProps } from 'next/link'
import { Link as MuiLink, LinkProps as MuiLinkProps } from '@mui/material'

// `LinkProps` is the combination of the MUI `LinkProps` and the Next `LinkProps`
// We wanna use the `href` prop from `next/link` so we omit it from MUI's.
export type LinkProps = Omit<MuiLinkProps, 'href'> &
  Omit<NextLinkProps, 'as' | 'passHref' | 'children'>

/**
 * A convenience component that wraps the MUI `Link` component that provides
 * our look & feel with Next's router `Link`
 *
 * @see https://next.js.org/docs/api-reference/next/link
 */
const Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
  { href, prefetch, replace, scroll, shallow, locale, ...muiProps },
  ref,
) {
  return (
    <NextLink
      href={href}
      replace={replace}
      scroll={scroll}
      shallow={shallow}
      locale={locale}
      passHref
    >
      <MuiLink ref={ref} {...muiProps} />
    </NextLink>
  )
})

export default Link

The main difference here is the LinkProps type definition.

type LinkProps = Omit<MuiLinkProps, 'href'> &
  Omit<NextLinkProps, 'as' | 'passHref' | 'children'>

It ensures that we only can pass in valid props for our new <Link> component. How it's defined is also important. First we take all the props of our component library's Link component (MuiLinkProps in this case), but omits the href prop. This is because it is also defined in NextLinkProps and we want to ensure that we use the type definition for href from next/link because it supports both a string as well as a URL object.

Then we intersect (or extend) all of the props from NextLinkProps. I personally also exclude the as prop because it's basically legacy functionality. We can omit passHref and children as well because we're explicitly setting them on <NextLink> (the children of <NextLink> is the <MuiLink>).

Lastly, we update forwardRef() to include the ref type and component props type as the generic params: forwardRef<HTMLAnchorElement, LinkProps>.

So now back in our home page component, we can use our new <Link> component.

import Link from '../components/Link'

const Home = () => {
  return (
    <ul>
      <li>
        <Link href="/">Home</Link>
      </li>
      <li>
        <Link href="/about">About Us</Link>
      </li>
      <li>
        <Link href="/blog/hello-world">Blog Post</Link>
      </li>
    </ul>
  )
}

export default Home

Now we're back to it feeling like we're just using our component library <Link> component, but with all of the bells and whistles of next/link. 🎉

One more thing before we finish. The next/link only works for local links. It does nothing for external links. In fact, using it for external links results in a bunch of wasted work. So it's better if we just use our component library's <Link> component directly.

import { Link as ExternalLink } from '@mui/material'
import Link from '../components/Link'

const Home = () => {
  return (
    <ul>
      <li>
        <Link href="/">Home</Link>
      </li>
      <li>
        <Link href="/about">About Us</Link>
      </li>
      <li>
        <Link href="/blog/hello-world">Blog Post</Link>
      </li>
      <li>
        <ExternalLink href="https://www.benmvp.com">Ben Ilegbodu</ExternalLink>
      </li>
    </ul>
  )
}

export default Home

I explicitly name the component library's link component ExternalLink to make it abundantly clear that it's only to be used for external links. Our custom <Link> component that wraps next/link is the default one to use.

Alternatively, I could update the custom Link component to be smarter and only render a <MuiLink> when the url is external, but then it would need all the logic to resolve the href to a string if it's a URL object and then detect whether or not a URL string is external. That's more work than I'm willing to put in. 😅


I have to create this wrapper Link component with every React Next.js project I work on that uses MUI. It makes me wonder if MUI should just add the next/link wrapper itself. Next.js is popular enough that I think it'd be worth it. But until then, this code snippet is simple enough to copy and paste. It's what we developers do best anyway. 😂

As always if you've got any questions or comments, feel free to reach out to me on Twitter at @benmvp.

Keep learning my friends. 🤓

Subscribe to the Newsletter

Get notified about new blog posts, minishops & other goodies


Hi, I'm Ben Ilegbodu. 👋🏾

I'm a Christian, husband, and father of 3, with 15+ years of professional experience developing user interfaces for the Web. I'm a Google Developer Expert Frontend Architect at Stitch Fix, and frontend development teacher. I love helping developers level up their frontend skills.

Discuss on Twitter // Edit on GitHub