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 inReact.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 withinforwardRef
still has a component name (Link
). It helps with debugging in the DevTools so that it'll sayForwardRef(Link)
instead of justForwardRef
.
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. 🤓