Push me

Last updated on May 30, 2020. Created on May 16, 2020.

To keep in the spirit of honing our skills, we’ll tackle one more component, a button.

So let’s start with the basics, a <button /> has children, an onClick handler, a type (submit, button or reset) and can be disabled.

One thing to keep in mind with buttons is that if you don’t supply a type, it will default to submit which is generally not what you want.

import React, { ReactNode } from 'react';

export type ButtonProps = {
  children: ReactNode;
  disabled?: boolean;
  onClick?: (
    event: React.MouseEvent<HTMLButtonElement, MouseEvent>
  ) => void;
  type: 'submit' | 'reset' | 'button';
};

function Button(props: ButtonProps): JSX.Element {
  const { children, disabled, onClick, type } = props;

  return (
    <button disabled={disabled} onClick={onClick} type={type}>
      {children}
    </button>
  );
}

export default Button;

Using the Pick utility type we could shorten the ButtonProps to the following:

export type ButtonProps = Pick<
  React.DetailedHTMLProps<
    React.ButtonHTMLAttributes<HTMLButtonElement>,
    HTMLButtonElement
  >,
  'children' | 'disabled' | 'onClick'
> & {
  /**
   * The reason we are not picking this is because react
   * allows undefined.
   */
  type: 'button' | 'submit' | 'reset';
};

Touch

We just need to add that bootstrap flavor, let’s add variant into the mix:

import React from 'react';
import classNames from 'classnames';

export type ButtonProps = Pick<
  React.DetailedHTMLProps<
    React.ButtonHTMLAttributes<HTMLButtonElement>,
    HTMLButtonElement
  >,
  'children' | 'disabled' | 'onClick'
> & {
  /**
   * The reason we are not picking this is because react
   * allows undefined.
   */
  type: 'button' | 'submit' | 'reset';
  variant:
    | 'primary'
    | 'secondary'
    | 'success'
    | 'danger'
    | 'warning'
    | 'info'
    | 'light'
    | 'dark'
    | 'link';
};
const BS_ROOT = 'btn';

function Button(props: ButtonProps): JSX.Element {
  const { children, disabled, onClick, type, variant } = props;

  return (
    <button
      className={classNames(BS_ROOT, `${BS_ROOT}-${variant}`)}
      disabled={disabled}
      onClick={onClick}
      type={type}
    >
      {children}
    </button>
  );
}

export default Button;

Satisfaction

But what if you wanted a “close” button which just an x inside it as children?

import React from 'react';
import Alert from './components/Button';

function App(): JSX.Element {
  return <Button type="button">&times;</Button>;
}

We should always have accessibility in mind when we create components, a button without a readable text is not accessible. The correct approach is to add an aria-label to the button.

And as an added advantage if you’re using React Testing Library, you can use a ByLabelText query and don’t have to add a data-testid to a button.

So we want to achieve this:

import React from 'react';
import Alert from './components/Button';

function App(): JSX.Element {
  return (
    <Button aria-label="Close" type="button">
      &times;
    </Button>
  );
}

We can again pick the aria-label from the button properties.

export type ButtonProps = Pick<
  React.DetailedHTMLProps<
    React.ButtonHTMLAttributes<HTMLButtonElement>,
    HTMLButtonElement
  >,
  'aria-label' | 'children' | 'disabled' | 'onClick'
> & {
  type: 'button' | 'submit' | 'reset';
  variant: 'primary'; //shortened
};

const BS_ROOT = 'btn';

function Button(props: ButtonProps): JSX.Element {
  const {
    'aria-label': ariaLabel,
    children,
    disabled,
    onClick,
    type,
    variant,
  } = props;

  return (
    <button
      aria-label={ariaLabel}
      className={classNames(BS_ROOT, `${BS_ROOT}-${variant}`)}
      disabled={disabled}
      onClick={onClick}
      type={type}
    >
      {children}
    </button>
  );
}

That’s it, i hope you are digging the typescript/react series so far.

Next

Kiss my default props

Previous

Practice makes purrfect