← Catalog

No. 008 · architecture

Component Patterns

UI components that don't turn into spaghetti

Version 1.0.0 License MIT Format SKILL.md

Most component codebases degrade because components accumulate responsibilities. A component that handles display, state, data fetching, and business logic becomes impossible to reuse, test, or reason about.

Composition over configuration

Bad — config Hell:

<Card
  title="..."
  variant="compact"
  showAvatar={true}
  avatarUrl="..."
  onClick={handler}
  badge="new"
  badgeColor="green"
  actions={[{ label: 'Edit', onClick: edit }]}
/>

Good — composition:

<Card>
  <Card.Header>
    <Avatar src="..." />
    <Card.Title>...</Card.Title>
    <Badge color="green">new</Badge>
  </Card.Header>
  <Card.Body>...</Card.Body>
  <Card.Footer>
    <Button onClick={edit}>Edit</Button>
  </Card.Footer>
</Card>

The composed version is more verbose but infinitely more flexible. Each piece can be rearranged, replaced, or omitted without changing the Card API.

Prop design principles

  1. Minimize required props. If a prop is almost always the same value, make it a default and let callers override it.

  2. Avoid booleans. isCompact, showHeader, hasBorder — these multiply combinatorially. Use variant or as instead:

    // Bad
    <Button isLarge isOutline isDisabled />
    
    // Good
    <Button variant="outline" size="large" disabled />
  3. Don’t pass objects for everything. A style prop is fine. Passing {{ padding: 10, margin: 5, background: 'red' }} as a prop means your component is actually a pass-through.

  4. Use children for content, props for configuration.

State placement

Push state to the lowest common ancestor that needs it:

  • Local state (useState/useReducer): UI state that doesn’t affect other components (hover, open/close, form input)
  • Context: state shared between distant components (theme, auth, locale)
  • External store (Zustand/Redux): complex state with many readers/writers, or state that survives page navigation
  • Server state (React Query/SWR): data from APIs — never store API responses in local state

When to split

Split a component when:

  • It exceeds 200 lines (not counting types/styles)
  • You can name two distinct sub-parts meaningfully
  • You’re passing more than 7 props
  • You find yourself writing if (props.variant === 'foo') in multiple places

See references/prop-patterns.md for more prop design patterns.

When it triggers

  • designing a new UI component
  • component has too many props
  • deciding where to put state
  • splitting a large component