How to make a reusable button component with Typescript in React applications
Tags: webdev, react, typescript, programming
Published: 2023-07-21 17:52:15

## Introduction

Do you find your reusable component getting messier as your project develops? If so, it might be a good time to start refactoring it!

In this article, I will share one way to quickly improve the readability and maintainability of a reusable component using a button component as an example.

## Cluttered button component

Here we have a reusable, but very cluttered button component. 

```
import { Link, LinkProps } from 'react-router-dom';
import React, { CSSProperties } from 'react';
import classNames from 'classnames';
import { FadeLoader } from 'react-spinners';

const spinnerOverride: CSSProperties = {
  margin: '0 auto',
  top: '30px'
};

interface ButtonProps {
  className?: string;
  onClick?: () => void;
  to?: LinkProps['to'];
  children?: React.ReactNode;
  inNav?: boolean;
  isPrimary?: boolean;
  isSecondary?: boolean;
  isWarning?: boolean;
  disabled?: boolean;
  isLoading?: boolean;
  type: 'button' | 'reset' | 'submit' | undefined;
  testId?: string;
}
export const Button: React.FC<ButtonProps> = ({
  className,
  onClick,
  to,
  inNav,
  isPrimary,
  isSecondary,
  isWarning,
  type,
  testId,
  disabled,
  isLoading,
  children
}) => {
  const hrefTo = to ?? '#';
  if (isLoading) {
    return (
      <FadeLoader
        data-testid="loading"
        loading={isLoading}
        height={10}
        width={10}
        cssOverride={spinnerOverride}
        aria-label="Loading Spinner"
      />
    );
  }

  if (to) {
    return (
      <Link
        className={classNames(
          {
            'pl-0 lg:pl-3 py-2 rounded-3xl hover:bg-gray-300 ease-in duration-300':
              inNav
          },
          {
            'w-full bg-primary-500 text-white inline-block p-2 rounded-lg hover:bg-opacity-75 focus:outline-none focus:ring focus:border-primary-800':
              isPrimary
          },
          {
            'w-full bg-secondary-500 text-white inline-block p-2 rounded-lg hover:bg-opacity-75 focus:outline-none focus:ring focus:border-secondary-800':
              isSecondary
          },
          {
            'w-full bg-error-500 text-white inline-block p-2 rounded-lg hover:bg-opacity-75 focus:outline-none focus:ring focus:border-error-800':
              isWarning
          },
          {
            'cursor-not-allowed': disabled
          },
          className
        )}
        to={hrefTo}
      >
        <button
          className={classNames(
            'w-full',
            { 'flex gap-4': inNav },
            {
              'cursor-not-allowed': disabled
            }
          )}
          data-testid={testId}
          tabIndex={0}
          onClick={onClick}
          disabled={disabled}
          type={type}
        >
          {children}
        </button>
      </Link>
    );
  }

  return (
    <button
      className={classNames(
        {
          'w-full bg-primary-500 text-white inline-block p-2 rounded-lg hover:bg-opacity-75':
            isPrimary
        },
        {
          'w-full bg-secondary-500 text-white inline-block p-2 rounded-lg hover:bg-opacity-75':
            isSecondary
        },
        {
          'w-full bg-error-500 text-white inline-block p-2 rounded-lg hover:bg-opacity-75':
            isWarning
        },
        {
          'py-2 lg:pl-3 rounded-3xl hover:bg-gray-300 ease-in duration-300 flex gap-4 w-full ':
            inNav
        },
        {
          'cursor-not-allowed': disabled
        },
        className
      )}
      data-testid={testId}
      tabIndex={0}
      onClick={onClick}
      disabled={disabled}
      type={type}
    >
      {children}
    </button>
  );
};

```
As a project develops, it's common for a reusable component to start accepting more and more props to extend its versatility. Depending on where it's used, we might want to pass different styles, types, IDs for testing, accessibility attributes, and more to a component.

But is it really necessary to explicitly write every single prop when we type, destructure, and consume them like this?

Absolutely not!

## Refactor a reusable component using JSX.IntrinsicElements

One way to improve the readability and maintainability of a reusable component is to leverage React types.

Let's perform some magic!

```
import { Link, LinkProps } from 'react-router-dom';
import React, { CSSProperties } from 'react';
import classNames from 'classnames';
import { FadeLoader } from 'react-spinners';

const spinnerOverride: CSSProperties = {
  margin: '0 auto',
  top: '30px'
};

type IntrinsicButtonProps = JSX.IntrinsicElements['button'];

interface ButtonProps extends IntrinsicButtonProps {
  to?: LinkProps['to'];
  inNav?: boolean;
  isPrimary?: boolean;
  isSecondary?: boolean;
  isWarning?: boolean;
  isLoading?: boolean;
}

export const Button: React.FC<ButtonProps> = ({
  className,
  to,
  inNav,
  isPrimary,
  isSecondary,
  isWarning,
  disabled,
  isLoading,
  children,
  ...props
}) => {
  const hrefTo = to ?? '#';
  if (isLoading) {
    return (
      <FadeLoader
        data-testid="loading"
        loading={isLoading}
        height={10}
        width={10}
        cssOverride={spinnerOverride}
        aria-label="Loading Spinner"
      />
    );
  }

  if (to) {
    return (
      <Link
        className={classNames(
          {
            'pl-0 lg:pl-3 py-2 rounded-3xl hover:bg-gray-300 ease-in duration-300':
              inNav
          },
          {
            'w-full bg-primary-500 text-white inline-block p-2 rounded-lg hover:bg-opacity-75 focus:outline-none focus:ring focus:border-primary-800':
              isPrimary
          },
          {
            'w-full bg-secondary-500 text-white inline-block p-2 rounded-lg hover:bg-opacity-75 focus:outline-none focus:ring focus:border-secondary-800':
              isSecondary
          },
          {
            'w-full bg-error-500 text-white inline-block p-2 rounded-lg hover:bg-opacity-75 focus:outline-none focus:ring focus:border-error-800':
              isWarning
          },
          {
            'cursor-not-allowed': disabled
          },
          className
        )}
        to={hrefTo}
      >
        <button
          className={classNames(
            'w-full',
            { 'flex gap-4': inNav },
            {
              'cursor-not-allowed': disabled
            }
          )}
          {...props}
        >
          {children}
        </button>
      </Link>
    );
  }

  return (
    <button
      className={classNames(
        {
          'w-full bg-primary-500 text-white inline-block p-2 rounded-lg hover:bg-opacity-75':
            isPrimary
        },
        {
          'w-full bg-secondary-500 text-white inline-block p-2 rounded-lg hover:bg-opacity-75':
            isSecondary
        },
        {
          'w-full bg-error-500 text-white inline-block p-2 rounded-lg hover:bg-opacity-75':
            isWarning
        },
        {
          'py-2 lg:pl-3 rounded-3xl hover:bg-gray-300 ease-in duration-300 flex gap-4 w-full ':
            inNav
        },
        {
          'cursor-not-allowed': disabled
        },
        className
      )}
      {...props}
    >
      {children}
    </button>
  );
};

```
Wow! 

Can you see the difference from the previous code?
The amount of code clearly reduced, making the component a lot easier to read.

So what have been changed?

### Change the type of the props

The biggest difference is how we declare the type of the props. 

```
type IntrinsicButtonProps = JSX.IntrinsicElements['button'];
```
First, this line of code extends `JSX.IntrinsicElements['button']`, which includes all the standard attributes available for a <button> element. 

```

interface ButtonProps extends IntrinsicButtonProps {
  to?: LinkProps['to'];
  inNav?: boolean;
  isPrimary?: boolean;
  isSecondary?: boolean;
  isWarning?: boolean;
  isLoading?: boolean;
}

```

And then, we declare a new interface `ButtonProps` by adding custom attributes to `IntrinsicButtonProps`, which already has the basic button attributes.

### Adjust the way to destructure and use props

The second difference is the way we destructure and use the props.

```
export const Button: React.FC<ButtonProps> = ({
  className,
  to,
  inNav,
  isPrimary,
  isSecondary,
  isWarning,
  disabled,
  isLoading,
  children,
  ...props
}) => {
```

```
<button
  className={classNames(
  'w-full',
  { 'flex gap-4': inNav },
  {
   'cursor-not-allowed': disabled
   }
  )}
  {...props}
>
 {children}
</button>
```

When destructing, we extract some items by explicitly writing each one of them while handling the rest by using spread operator.

The extraction is useful for an item used conditionally as you can see in my example. We pass the rest to `button` tag by again using spread operator.

That's it!

## Final remark

I believe that enhancing the versatility of a reusable component necessities some refactoring.

Using the `JSX.IntrinsicElements` is one of the approach and we already see a huge improvement in code.

What is your strategy to extend the versatility of a reusable component without compromising the readability and maintainability?

I am keen to know!