DEV Community

Cover image for How to Build a Reusable Button Component in React with TypeScript
Sandhya-Allgandhuala
Sandhya-Allgandhuala

Posted on

How to Build a Reusable Button Component in React with TypeScript

Introduction

When I started building my Smart Budget Tracker app, I noticed I was copy-pasting button code everywhere - submit buttons, link buttons, loading buttons. Each one looked slightly different. That's when I decided to build one reusable button component to rule them all.

In this post I'll walk you through how I built it using React and TypeScript.


What We are Building

A single component that handles:

  1. Regular click buttons
  2. From submit buttons
  3. Navigation link buttons
  4. Loading state with a spinner
  5. Disabled state
  6. Multiple sizes and variants

Step 1 - Defines the Props Interface

The first thing I do in Typescript is define exactly what the component accepts. This gives you autocomplete and catches mistakes at compile time.

interface Props {
  label: string;
  onClick?: () => void;
  disabled?: boolean;
  loading?: boolean;
  href?: string;
  variant?: 'default' | 'primary';
  size?: 'sm' | 'md' | 'lg';
  fullWidth?: boolean;
  type?: 'submit' | 'button';
  className?: string;
}

The ? means the prop is optional, only label is required everything else has a default.


Step 2 - Set Default Values

function Button({
  label,
  onClick,
  disabled = false,
  loading = false,
  href,
  variant = 'default',
  size = 'md',
  fullWidth = false,
  type = 'button',
  className = '',
}: Props) {

Default values mean callers don't need to pass every prop, <Button label = "Save"/> just works.


Step 3 - Build the CSS class dynamically

Instead of writing if/else for every style combination, I build the class string from an array

const isDisabled = disabled || loading;

const wrapperCSSClass = [
  'btn-base',
  `btn-${variant}`,
  `btn-${size}`,
  fullWidth ? 'btn-full': '',
  isDisabled ? 'btn-disabled': '',
  className,
].filter(Boolean).join(' ');

filter(Boolean) removes any empty string so you don't get extra spaces in the class name. Adding a new variant is just one line.


Step 4 - Handle the Loading Spinner

When Loading is true, I show a spinner SVG icon and change the label text

const labelContent = (
  <>
    {loading && (
      <svg className="animate-spin h-4 w-4" ...>
        ...
      </svg>
    )}
    <span>{loading ? 'Loading...': label}</span>
  </>
);

I also added aria- busy = {loading} on the button element. This tells screen readers that the button is busy - a small but important accessibility detail.


Step 5 - Handle Link Vs Button

This was the interesting part, sometimes a button navigates to another pages like a "Registration" link that looks like a button. I handle both cases:

if (href) {
  return isDisabled
    ? <span className={wrapperCSSClass}>{labelContent}</span>
    : <Link to={href} className={wrapperCSSClass}>{labelContent}</Link>;
}

return (
  <button
    type={type}
    onClick={onClick}
    disabled={isDisabled}
    className={wrapperCSSClass}
    aria-busy={loading}
  >
    {labelContent}
  </button>
);

When href is passed, it renders a React Router <Link>. When disabled, it renders a <span> because a disabled link is semantically incorrect in HTML.


How to Use it

// Primary submit button
<Button label="Log in" type="submit" variant="primary" fullWidth loading={loading} />

// Navigation link styled as button
<Button label="Register Free" href="/Register" />

// Disabled button
<Button label="Save" disabled />

What I Learned

  1. TypeScript interfaces makes component self-documenting - you always know what props are available
  2. filter(Boolean) is a clean trick for building dynamic class strings
  3. One component can handle both <button> and <Link>with a simple conditional render
  4. Accessibility (aria-busy) is easy to add and makes a real difference

What's Next

In my next post l'll cover how I built a reusable TextInput component with error state, icons, and password toggle - also from my Smart Budget Tracker project.


If this helped you, drop a like or comment. I'm just getting started with blogging and any feedback is welcome!



Top comments (0)