DEV Community

mihomihouk
mihomihouk

Posted on

How to make a reusable button component with Typescript in React applications

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>
  );
};

Enter fullscreen mode Exit fullscreen mode

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>
  );
};

Enter fullscreen mode Exit fullscreen mode

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'];
Enter fullscreen mode Exit fullscreen mode

First, this line of code extends JSX.IntrinsicElements['button'], which includes all the standard attributes available for a element.


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

Enter fullscreen mode Exit fullscreen mode

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
}) => {
Enter fullscreen mode Exit fullscreen mode
<button
  className={classNames(
  'w-full',
  { 'flex gap-4': inNav },
  {
   'cursor-not-allowed': disabled
   }
  )}
  {...props}
>
 {children}
</button>
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)