Polymorphism
In order to build usable, accessible interfaces, it's important that we understand the semantics of different HTML tags.
For example: if an element can be clicked to perform an action in JS, it should be a button! Unless that action is to navigate the user to a new page, in which case, it should be an anchor (<a>
).
(If you'd like to learn more about why adding a click-handler to a <div>
is such a bad idea, I recommend checking out React Podcast #34, “Just Use A Button”, with superstar developer Jen Luker. The entire episode is worth listening to, but the button-specific part is around 26:30.)
When choosing an HTML tag, it's much more important to focus on the semantics than the aesthetics. You should use a <button>
even if you don't want it to look like a button. With CSS, we can strip away all of those built-in button styles. It's much easier to remove a handful of CSS rules than it is to recreate all of the usability benefits built into the <button>
tag.
With all of that in mind, let's suppose our designer wants us to build the following UI:
In the top right, there are some actions the user can take:
These look like links, but are they? It depends on whether clicking them changes the URL or not. “Export All Data” doesn't sound like a link to me; I imagine it generating a .csv and emailing it to the user.
So, here's what we're going to do. We're going to build a LinkButton
component. It's always going to look like a link, but it's going to be flexible in its implementation: it can either render an <a>
tag, or a <button>
tag, depending on whether an href
is supplied.
Spend a few minutes tinkering, and then watch the video below to see how I'd approach this problem.
Acceptance Criteria:
- The
LinkButton
component has an optional prop,href
. - If an
href
is provided,LinkButton
should render an<a>
tag. Otherwise, it should render a<button>
tag.
Code Playground
Let's explore:
Video Summary
So there's a few ways we could solve this problem.
We could create a separate "branch" based on the href
:
function LinkButton({ href, children,}) { // Branch 1: anchor if (href) { return ( <a href={href} className={styles.button} > {children} </a> ); }
// Branch 2: button return ( <button className={styles.button}> {children} </button> );}
This works, but it's a bit of a bummer. Whenever I make a change to this component, I have to remember to edit both branches. For example, adding a ...delegated
object:
function LinkButton({ href, children, ...delegated}) { // Branch 1: anchor if (href) { return ( <a href={href} className={styles.button} {...delegated} > {children} </a> ); }
// Branch 2: button return ( <button className={styles.button} {...delegated} > {children} </button> );}
Instead, I prefer to solve this problem using a technique known as polymorphism.
The first time you see this approach, it looks a little funky, but fear not! I'll explain everything.
First, here's what it looks like:
function LinkButton({ href, children, ...delegated}) { const Tag = typeof href === 'string' ? 'a' : 'button';
return ( <Tag href={href} className={styles.button} {...delegated} > {children} </Tag> );}
To understand what's going on here, it's helpful to look at the plain JS one, without the JSX making it harder to understand:
// This code:React.createElement( Tag, { href, className: styles.button, ...delegated }, children);
// Is the same as this code:<Tag href={href} className={styles.button} {...delegated}> {children}</Tag>
The Tag
variable will resolve either to the string "a"
or "button"
. The type is dynamic, but either way, it's a string.
It's confusing because we generally reserve uppercase variable names for components like App
or Slider
. So it's weird that an uppercase variable is rendering a "native" DOM node.
Why does Tag
have to be uppercase? Wouldn't it be more natural to make our variable "tag"
instead?
Well, if we try that, we'll render literal <tag>
HTML elements. 😬
<tag href="/add-transaction" class="button"> Add Transaction</tag>
As we learned in Module 1, lowercase JSX tags are treated verbatim, while capitalized JSX tags are treated dynamically:
<Button/> // React.createElement(Button)<button/> // React.createElement('button')
We need to use a capital letter so that the JSX compiler creates an element using the Tag
variable, and not the "tag"
string.
I know that this pattern is a bit of a headscratcher the first time you see it, but I think it's a pretty elegant solution to a tough problem.
Here's the final code from the video: