Go Back

Button or A

Posted: 

I like to be able to abstract away the complexity of implementation and focus on the use. When this comes to buttons or anchors, I want my developers to be able to use the component that looks like a button or link and not worry about what html tag it renders.

Polymorphic components

This is called polymorphism when we're referring to components. It allows the underlying html tag to be different based on various conditions, like a prop.

Some components use the as prop where you can specify which tag you want the component to output. This is ok but requires the developer to pick the right option for the situation. I wanted to see if it could pick the right one based on context.

href is the key

Thought there may be other subtle properties that differentiate a button from an anchor, the href prop is the one we care about.

Basically, if an href is specified then the rendered HTML should be an <a>, otherwise it's a <button>. The component you pick, whether <Button> or <Link> should always look like a button or link, even if the underlying HTML is different.

TypeScript

I was fairly new to TypeScript when I first built this component so I didn't fully understand what was going on but copied from other implementations to get it to work.

Now I have a deeper understanding and want to look at it again.

The key feature that makes this work is the type guard. This is a function that returns a type predicate. I'll get into these in a bit.

ButtonOrA

I don't know that this is the best naming of the component, but I'm in favor of using names that are as descriptive as possible as to what the component does.

This will act as the based component for a number of higher level components including:

  • <Button>
  • <Link>
  • <MenuButton>
  • <FooterButton>

Essentially any component that has a specific look and style but you want it to choose between a <button> or a <a> when it renders. I don't want to have to duplicate that logic in all those components.

On to the code

Here is the code for this component that we'll be going over:

import { Ref, forwardRef } from 'react';

// Button props
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
  href?: undefined;
};

// Anchor props
type AnchorProps = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
  href?: string;
  disabled?: boolean;
};

export type ButtonOrAProps = ButtonProps | AnchorProps;
export type ButtonOrARef = HTMLButtonElement | HTMLAnchorElement;

const hasHref = (props: ButtonOrAProps): props is AnchorProps =>
  'href' in props && props.href !== undefined;

const ButtonOrA = forwardRef<ButtonOrARef, ButtonOrAProps>((props, ref) => {
  // anchor render
  if (hasHref(props)) {
    // eslint-disable-next-line @typescript-eslint/no-unused-vars -- a tag can't accept disabled prop
    const { disabled, ...restProps } = props;

    // eslint-disable-next-line jsx-a11y/anchor-has-content -- this is a base comopnent, accessibility should be handled by the parent
    return <a {...restProps} ref={ref as Ref<HTMLAnchorElement>} />;
  }
  return <button {...props} ref={ref as Ref<HTMLButtonElement>} />;
});

ButtonOrA.displayName = 'ButtonOrA';

export { ButtonOrA };

Let's go over this.

Prop types

// Button props
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
  href?: undefined;
};

// Anchor props
type AnchorProps = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
  href?: string;
  disabled?: boolean;
};

We need to define the prop types for both the Button and the Anchor. We define the href for both so that we have something for our type guard to test against.

The disabled prop on AnchorProps was added since our <Button> component may look like a button but render as an <a>. An anchor doesn't support the disabled attribute so we need to add it. We then remove it before we apply the props to the <a> and use it just for styling purposes.

Type guard

const hasHref = (props: ButtonOrAProps): props is AnchorProps =>
  'href' in props && props.href !== undefined;

This is a function that returns a type predicate. A type predicate is basically a way for TypeScript to use the result of a function to determine the type. In this case we're testing if href is in the props and telling TypeScript that means we should use the AnchorProps type.

Render depending on the result of hasHref

It's mostly straightforward from that point. We test the result of hasHref and render different output based on the result. Types all match up and we're good to go!

Deep TypeScript

This was a hard concept to get early on in my TypeScript career. It's still a bit tricky but getting easier all the time. Hopefully this helps you understand a little bit of this difficult topic.