Saved
Frontend Pattern

Props and Attributes

Pass data and configuration into components through properties or attributes.

Difficulty Beginner

By Den Odell

Props and Attributes

Problem

Components hardcode their data, text labels, and behavior directly in their implementation, making them impossible to reuse across different contexts with different requirements. Every variation requires duplicating the entire component with slightly modified hardcoded values, creating maintenance nightmares. Testing becomes painful because you cannot easily inject different values to verify edge cases, error states, or boundary conditions without modifying component source code.

Components cannot adapt to different languages, themes, or user preferences because configuration is baked in rather than passed from outside. Parent components cannot control child component behavior, forcing duplication when similar components need different data. Component libraries cannot be shared across projects because components are too specific to one use case, while a button component that says “Submit” cannot be reused for “Cancel” or “Delete” actions.

Solution

Accept data and configuration through props (component properties) or attributes (HTML attributes) that parent components pass in, allowing the same component implementation to serve different purposes based on external input. Props enable JavaScript values like objects, arrays, functions, and complex data structures to be passed into components, while attributes handle string values through HTML. Distinguish between JavaScript properties and HTML attributes when building web components - props offer JavaScript API access while attributes provide declarative HTML configuration. Define clear component interfaces with required vs optional props, default values for optional configuration, and type validation to catch incorrect usage. Use prop spreading cautiously to pass multiple values efficiently while maintaining explicit interfaces. This creates reusable, configurable components that adapt to different contexts without modification.

Example

This example shows a reusable button component that accepts configuration through props, making it adaptable to different contexts with different labels, handlers, and states.

React Props with Default Values

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

// Same component used in different ways with different props
<Button label="Submit" onClick={handleSubmit} />
<Button label="Cancel" onClick={handleCancel} variant="secondary" />
<Button label="Delete" onClick={handleDelete} variant="danger" disabled={loading} />
<Button label="Save" onClick={handleSave} icon={<SaveIcon />} size="large" />

React Props Destructuring and Spreading

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

// Usage with additional HTML attributes
<Card 
  title="User Profile" 
  className="profile-card"
  id="user-card-1"
  data-testid="profile-card"
  role="region"
  aria-labelledby="card-title"
>
  <UserDetails />
</Card>

TypeScript Props with Types

interface ButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean;
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'small' | 'medium' | 'large';
  icon?: React.ReactNode;
}

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

Vue Props with Validation

<template>
  <button 
    :class="buttonClasses"
    :disabled="disabled" 
    @click="$emit('click')"
  >
    <span v-if="icon" class="btn-icon">{{ icon }}</span>
    {{ label }}
  </button>
</template>

<script>
export default {
  props: {
    label: {
      type: String,
      required: true
    },
    disabled: {
      type: Boolean,
      default: false
    },
    variant: {
      type: String,
      default: 'primary',
      validator: (value) => {
        return ['primary', 'secondary', 'danger'].includes(value);
      }
    },
    size: {
      type: String,
      default: 'medium',
      validator: (value) => {
        return ['small', 'medium', 'large'].includes(value);
      }
    },
    icon: String
  },
  emits: ['click'],
  computed: {
    buttonClasses() {
      return `btn btn-${this.variant} btn-${this.size}`;
    }
  }
};
</script>

<!-- Usage -->
<Button 
  label="Submit" 
  variant="primary" 
  size="large"
  @click="handleSubmit" 
/>

Vue Composition API with Props

<script setup>
import { computed } from 'vue';

const props = defineProps({
  label: {
    type: String,
    required: true
  },
  disabled: {
    type: Boolean,
    default: false
  },
  variant: {
    type: String,
    default: 'primary'
  }
});

const emit = defineEmits(['click']);

const buttonClasses = computed(() => {
  return `btn btn-${props.variant}`;
});
</script>

<template>
  <button 
    :class="buttonClasses"
    :disabled="disabled"
    @click="emit('click')"
  >
    {{ label }}
  </button>
</template>

Svelte Props with Defaults

<script>
  export let label;
  export let disabled = false;
  export let variant = 'primary';
  export let size = 'medium';
  export let icon = null;
  
  $: buttonClasses = `btn btn-${variant} btn-${size}`;
</script>

<button 
  class={buttonClasses}
  {disabled} 
  on:click
>
  {#if icon}
    <span class="btn-icon">{icon}</span>
  {/if}
  {label}
</button>

<!-- Usage -->
<Button 
  label="Submit" 
  variant="primary" 
  size="large"
  on:click={handleSubmit} 
/>

Web Components - Props vs Attributes

class Button extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    // JavaScript properties (can be any type)
    this._onClick = null;
    this._data = null;
  }

  // Define which HTML attributes to observe
  static get observedAttributes() {
    return ['label', 'disabled', 'variant'];
  }

  connectedCallback() {
    this.render();
    this.shadowRoot.querySelector('button').addEventListener('click', () => {
      if (this._onClick) {
        this._onClick();
      }
      this.dispatchEvent(new CustomEvent('button-click'));
    });
  }

  // Called when observed attributes change
  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      this.render();
    }
  }

  // JavaScript property setters (for complex data)
  set onClick(handler) {
    this._onClick = handler;
  }

  set data(value) {
    this._data = value;
  }

  get data() {
    return this._data;
  }

  render() {
    const label = this.getAttribute('label') || 'Button';
    const disabled = this.hasAttribute('disabled');
    const variant = this.getAttribute('variant') || 'primary';

    this.shadowRoot.innerHTML = `
      <style>
        .btn { padding: 8px 16px; }
        .btn-primary { background: blue; color: white; }
        .btn-secondary { background: gray; color: white; }
      </style>
      <button 
        class="btn btn-${variant}"
        ${disabled ? 'disabled' : ''}
      >
        ${label}
      </button>
    `;
  }
}

customElements.define('custom-button', Button);

// Usage with attributes (strings)
const button = document.createElement('custom-button');
button.setAttribute('label', 'Click me');
button.setAttribute('variant', 'primary');

// Usage with properties (any type)
button.onClick = () => console.log('Clicked!');
button.data = { userId: 123, action: 'submit' };

Props with Children

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

// Usage with children prop
<Card 
  title="User Profile" 
  footer={<Button label="Save" />}
  variant="outlined"
>
  <UserForm />
</Card>

Render Props Pattern

function DataLoader({ url, render, renderLoading, renderError }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [url]);

  if (loading) return renderLoading ? renderLoading() : <div>Loading...</div>;
  if (error) return renderError ? renderError(error) : <div>Error</div>;
  return render(data);
}

// Usage
<DataLoader 
  url="/api/users"
  render={(users) => <UserList users={users} />}
  renderLoading={() => <Skeleton />}
  renderError={(err) => <ErrorMessage error={err} />}
/>

Prop Validation in Development

import PropTypes from 'prop-types';

Button.propTypes = {
  label: PropTypes.string.isRequired,
  onClick: PropTypes.func.isRequired,
  disabled: PropTypes.bool,
  variant: PropTypes.oneOf(['primary', 'secondary', 'danger']),
  size: PropTypes.oneOf(['small', 'medium', 'large']),
  icon: PropTypes.node,
  children: PropTypes.node
};

Button.defaultProps = {
  disabled: false,
  variant: 'primary',
  size: 'medium',
  icon: null
};

Benefits

  • Makes components reusable by accepting configuration through props, enabling the same component to serve different purposes across the application.
  • Simplifies testing by allowing easy injection of different values, edge cases, and error states without modifying component source code.
  • Enables composition where components can be configured for different contexts, themes, languages, and user preferences dynamically.
  • Creates clear, predictable component interfaces with explicit contracts about what data components expect and how they behave.
  • Facilitates component library development where generic components can be shared across projects and configured per-project needs.
  • Improves maintainability by centralizing component logic in one place while allowing variation through props rather than duplication.
  • Enables Storybook documentation showing all component variations through different prop combinations.

Tradeoffs

  • Can lead to prop explosion when components become too configurable, requiring dozens of props to handle every edge case and variation.
  • May complicate component internals with conditional logic based on props, making components harder to understand and maintain as complexity grows.
  • Requires careful API design to balance flexibility and simplicity - too few props limits reusability, too many creates cognitive overhead.
  • Can make components harder to understand with many configuration options, especially when props interact in complex ways.
  • Prop drilling becomes painful when data must be passed through many levels of components to reach deeply nested children.
  • Default values can hide bugs when incorrect values fall back to defaults silently rather than failing explicitly.
  • TypeScript or PropTypes add boilerplate and build complexity, though they prevent runtime errors from invalid props.
  • Props that accept functions create coupling between parent and child components that can make refactoring difficult.
  • Boolean props with negative semantics (disabled, hidden) can be confusing - disabled={false} reads awkwardly.
  • Spreading props ({...rest}) loses type safety and makes it unclear what props component actually uses, creating opaque interfaces.
  • Props are immutable within components - to modify prop values requires lifting state up or converting to internal state, adding complexity.
  • Web component attributes only support strings, requiring JSON serialization for complex data or separate property setters for JavaScript API.
  • Attribute vs property distinction in web components confuses developers - changing an attribute doesn’t update the property automatically without manual synchronization.
  • Required props create fragile dependencies where components fail at runtime if parents forget to pass them, though TypeScript helps catch this at compile time.
  • Optional props with many combinations create exponential test scenarios - testing all permutations becomes impractical.
  • Prop naming conflicts can occur when spreading props - collisions between explicit props and spread props cause subtle bugs.
Stay Updated

Get New Patterns
in Your Inbox

Join thousands of developers receiving regular insights on frontend architecture patterns

No spam. Unsubscribe anytime.