Use polymorphism to ditch the legacyBehavior prop in Next.js

TL;DR: Use polymorphism
A small history about the Link component
In the old days of Next.js 12, the Link component usage was a bit different from how it is today. You needed to wrap your <a>
tag with a <Link>
component and move the href
attribute to the Link
. Resulting in something like this:
// The Next.js 12 way
<Link href="/">
<a>The link</a>
</Link>
Today it looks like nonsense, but we could assume that some important reason was behind this decision. And at least, it was easy to implement.
But if you have something more complex, like a component with styles that should be reutilized, for example in a navigation bar, we needed to do something even weirder. The component needed to use the forwardRef
method and we still need to wrap each instance with the <Link href="/">
component, but this time with a key difference: a new prop is needed. You had to specify that you want to pass the href
prop to your component. What a mess.
// Still the Next.js 12 way
const Button = React.forwardRef(({ href, children }, ref) => {
return (
<a ref={ref} href={href} className="bg-black text-white rounded px-2 py-1">
{children}
</a>
);
});
const Navigation = () => {
return (
<ul>
<li>
<Link href="/" passHref>
<Button>One page</Button>
</Link>
</li>
<li>
<Link href="/" passHref>
<Button href="/">Another page</Button>
</Link>
</li>
</ul>
);
});
When Next.js 13 was released, by the end of 2022, they released a new approach. You don't need to use the nested <a>
inside the <Link>
anymore. Much more clean of course. But what happens with the custom component that we already have? The component can stay as it was, but since the behavior was outdated, they prepared a new prop to maintain the same behavior. This prop is called legacyBehavior
, and the usage is this:
// Next.js 13 - 15 way
<Link href="/" legacyBehavior passHref>
<Button>One page</Button>
</Link>
They wanted to simplify, but they made it actually worse in some scenarios because you needed even one more prop. If you were using styled components, if you were using another libraries, or you have your custom components. You were getting into trouble.
The legacyBehavior prop was intended as a transition thing, like: we allow you to upgrade to Next.js 13, but keep in mind that this behavior should change.
In further releases, a warning about the legacyBehavior
prop was marked as deprecated, and now with the release of Next 16 it will disappear completely. Hope you have updated everything in your codebase.
Then... what's the solution with our code?
Polymorphism at the rescue
Our custom button can be changed to something like this:
const ButtonA = ({ as: Component = "button", children, ...props }) => {
return (
<Component href={href} className="bg-black text-white rounded px-2 py-1">
{children}
</Component>
);
});
The differences are that we no longer need the forwardRef
usage and now our component doesn't render an <a>
tag, renders whatever we need.
We need a button?
// button is the default value
<Button onClick={/** do whatever **/}>
Do things
</Button>
We need a link that looks like a button?
<Button as="a" href="/somewhere">
Go somewhere
</Button>
But what about Next.js? We can do the same
// Next.js 16 way (required since 16, but actually allowed since 13)
import Link from "next/link";
// ...
<Button as={Link} href="/somewhere">
Go somewhere
</Button>;
An ending
Some people are very upset with this kind of change, as if the changes were made only to annoy them. My point of view is that it’s normal that libraries and frameworks evolve and change how they should be used. Everything changes, and if you keep your code as up to date as you can, with patterns like this or with slots like Radix / Shadcn do, it's always easier to perform smooth framework upgrades.