Skip to main content
Saved
Pattern
Difficulty Beginner

Props and Attributes

Pass data and configuration into components through properties or attributes.

By Den Odell Added

Props and Attributes

Problem

You need Submit, Cancel, and Delete buttons that look identical except for the label, but without props, you’re stuck copying nearly identical component files, changing one word, and repeating everywhere.

I’ve seen codebases with SubmitButton.tsx, CancelButton.tsx, DeleteButton.tsx, SaveButton.tsx, UpdateButton.tsx, ConfirmButton.tsx… you get the idea. All doing the same thing with different hardcoded text.

Testing becomes a nightmare too. You can’t inject test data because the component fetches its own, and simulating error states is nearly impossible when everything is hardcoded internally.

Solution

Pass data in from the outside instead of hardcoding it inside. That’s what props are for. A Button component accepts a label prop; pass label="Submit" or label="Cancel" and you have one component to maintain instead of twenty.

Props can be anything: strings, numbers, objects, arrays, functions. Pass user data, click handlers, and style variants, and the component renders accordingly.

Add sensible defaults so consumers don’t specify everything. A variant might default to “primary,” and disabled to false. Required props should throw errors if missing; TypeScript makes this easy.

For web components, distinguish between HTML attributes (always strings) and JavaScript properties (any type). Attributes work in declarative HTML; properties are for the JavaScript API.

Example

Here’s a Button component that handles every use case (different labels, variants, sizes, icons) all through props.

Basic Props

function Button({ label, onClick, disabled = false, variant = 'primary', size = 'medium', icon = null }) {
  return (
    <button
      className={`btn btn-${variant} btn-${size}`}
      onClick={onClick}
      disabled={disabled}
    >
      {icon && <span className="btn-icon">{icon}</span>}
      {label}
    </button>
  );
}

<Button label="Submit" onClick={handleSubmit} />
<Button label="Cancel" onClick={handleCancel} variant="secondary" />
<Button label="Delete" onClick={handleDelete} variant="danger" disabled={loading} />

Props Spreading

function Card({ title, children, className, ...rest }) {
  return (
    <div className={`card ${className || ''}`} {...rest}>
      <h2>{title}</h2>
      <div className="card-content">{children}</div>
    </div>
  );
}

<Card title="User Profile" className="profile-card" data-testid="profile-card">
  <UserDetails />
</Card>

TypeScript Props

interface ButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean;
  variant?: 'primary' | 'secondary' | 'danger';
}

function Button({ label, onClick, disabled = false, variant = 'primary' }: ButtonProps) {
  return (
    <button className={`btn btn-${variant}`} onClick={onClick} disabled={disabled}>
      {label}
    </button>
  );
}

Props with Children

function Card({ title, footer, children }) {
  return (
    <div className="card">
      {title && <div className="card-header"><h3>{title}</h3></div>}
      <div className="card-body">{children}</div>
      {footer && <div className="card-footer">{footer}</div>}
    </div>
  );
}

<Card title="User Profile" footer={<Button label="Save" />}>
  <UserForm />
</Card>

Benefits

  • One component, infinite uses. The same Button works for “Submit,” “Cancel,” “Delete” with just a prop change.
  • Testing becomes trivial. Pass mock data as props, verify the output. No internal state to fight with.
  • Components become reusable across projects. A well-designed Button component works anywhere.
  • Storybook loves props. Document every variant by passing different prop combinations.
  • TypeScript catches mistakes at compile time. Pass the wrong type to a prop and your editor screams at you before runtime.
  • Logic stays centralized. Fix a bug once in the component, and all consumers benefit.

Tradeoffs

  • Prop explosion is real. Add enough configuration options and you end up with components that take twenty props. The API becomes unwieldy.
  • Conditional logic piles up. Every prop adds another if statement inside the component. Complexity grows.
  • Prop drilling gets tedious. Passing data through five layers of components to reach a deeply nested child is annoying and error-prone.
  • Default values can hide bugs. If someone passes undefined and it silently falls back to a default, you might not notice the upstream problem.
  • Boolean props read awkwardly. disabled={false} is harder to parse than just omitting the prop entirely.
  • Spreading props ({...rest}) loses type safety. You don’t know what’s being passed through.
  • Web component attributes only support strings. Complex data needs JSON serialization or separate property setters.
  • The more optional props you have, the more combinations you need to test. Exponential test scenarios are no fun.

Summary

Props transform a single component into a flexible building block that serves multiple use cases through configuration rather than duplication. Pass data, callbacks, and styling variants as props to create components that work anywhere without internal assumptions about where their data comes from. Add sensible defaults for optional props and TypeScript types for safety, keeping your component API clear and predictable.

Newsletter

A Monthly Email
from Den Odell

Behind-the-scenes thinking on frontend patterns, site updates, and more

No spam. Unsubscribe anytime.