惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

宝玉的分享
宝玉的分享
B
Blog RSS Feed
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
MyScale Blog
MyScale Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
S
SegmentFault 最新的问题
Y
Y Combinator Blog
月光博客
月光博客
IT之家
IT之家
T
Tailwind CSS Blog
Last Week in AI
Last Week in AI
L
LangChain Blog
博客园_首页
MongoDB | Blog
MongoDB | Blog
P
Proofpoint News Feed
博客园 - Franky
WordPress大学
WordPress大学
云风的 BLOG
云风的 BLOG
M
MIT News - Artificial intelligence
V
Visual Studio Blog
小众软件
小众软件
博客园 - 叶小钗
博客园 - 三生石上(FineUI控件)
N
Netflix TechBlog - Medium

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
How to Build a Reusable Button Component in React with Ty...
Sandhya-Allgandhuala · 2026-06-23 · via DEV Community

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!