Lesson 66 of 80
One Button component that can render as a button, an anchor, or your router's Link — without three separate components duplicating the same styles.
14 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
A design system's Button usually needs to render as more than one HTML element: a real <button> for actions, an <a> for navigation that should look identical, sometimes a router's own <Link> so client-side navigation works. Writing Button, LinkButton, and RouterButton as three separate components means the same padding, colors, and hover states get defined three times and drift apart the moment one gets updated without the others.
A polymorphic component takes an as prop that says which underlying element (or component) to actually render, while keeping one single implementation of the styling and behavior.
import { forwardRef } from "react";
const Button = forwardRef(function Button(
{ as: Component = "button", className, ...props },
ref
) {
return (
<Component
ref={ref}
className={`btn ${className ?? ""}`}
{...props}
/>
);
});
// Renders a real <button>
<Button onClick={handleSave}>Save</Button>
// Renders an <a> — same styles, different element
<Button as="a" href="/docs">Read the docs</Button>
// Renders your router's Link — same styles, client-side navigation
<Button as={Link} to="/dashboard">Go to dashboard</Button>
Component here is just a variable holding a JSX tag — "a" and Link are both valid values because JSX capitalization rules treat a lowercase string as an HTML tag and a capitalized identifier as a component, and both are legal in this position. Button never hardcodes which one it is; it forwards whatever as provides and gives it every other prop untouched (href, to, onClick — whatever that particular element actually needs).
The forwardRef here isn't optional set dressing — a parent reaching for <Button ref={btnRef}> needs that ref to land on the real underlying <button> or <a> DOM node, not on Button itself, exactly the same forwarding problem any custom component with a ref has.
as isn't a React feature — it's a convention. Nothing about React treats a prop named as specially; it's just a prop whose value happens to be used as the JSX tag. The pattern works because JSX allows a variable there at all, not because of anything React-specific to the name as.href, to, onClick, aria-label — none of them need to be declared on Button itself; {...props} forwards whatever the caller passed to whatever Component turns out to be.Heading that only ever renders <h1> through <h6> earns a polymorphic as prop; a component that only ever renders one specific thing doesn't need the flexibility "just in case."This pattern leans directly on ref forwarding from earlier in this track — the exact mechanism that lets a parent's ref reach through a custom component to the real DOM node underneath, which here is whichever DOM node as happens to resolve to. It's also just props doing what props always do: as isn't special-cased machinery, it's a value passed at the call site like any other, used as a JSX tag instead of a string or number.