Frontend

Component Design Principles

A component that is 30 lines today becomes a nightmare six months later, with ten props and five nested conditions. This article covers the principles that keep component writing maintainable, with React examples that apply to other frameworks as well.

  • By TETA Team
  • 8 min read
  • Updated:
Component Design Principles

What makes a good component?

A good component is small, predictable and composable: given known inputs (props) it produces known output, and you can tell what it does from the outside without reading its code. Our practical test is whether a teammate can use it correctly from its name and props alone.

These principles are tool-agnostic, but our examples use React and TypeScript, because typed props are the best living documentation a component has. If you are still wrestling with where to keep data in a UI, read our earlier piece on React state management too.

Principle 1: single responsibility

The sign of a component doing too much is simple: describing it takes an "and". "It fetches products and filters them and renders them and updates the cart." Split it into parts with separate roles: data fetching, logic and presentation.

ts
// Presentation only: easy to test and reuse
function ProductCard({ name, price, onAdd }: {
  name: string; price: number; onAdd: () => void;
}) {
  return (
    <article>
      <h3>{name}</h3>
      <p>{price.toLocaleString()}</p>
      <button type="button" onClick={onAdd}>Add</button>
    </article>
  );
}

// Logic lives in a hook, not in the markup
function useProducts(category: string) { /* fetch + cache */ }

Put fetching and transformation logic in a hook or service, and keep the presentational component as dumb as possible. The result is something you can render without a network and in an isolated sandbox such as Storybook.

Principle 2: design props carefully

Props are your component's public API, and every extra prop is a maintenance commitment. A few rules that help a lot:

  • Replace several overlapping booleans with a union: variant: "primary" | "ghost" | "danger" instead of isPrimary, isGhost and isDanger.
  • Start event props with on and internal handlers with handle: onSelect in the API, handleSelect inside.
  • Give sensible defaults so the simplest usage is also the shortest.
  • Do not pass a whole large object; pass only the fields needed. It cuts coupling and needless re-renders.
  • Type your props explicitly so others can discover them.
ts
type ButtonProps = {
  variant?: "primary" | "ghost" | "danger";
  size?: "sm" | "md";
  loading?: boolean;
  children: React.ReactNode;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;

function Button({ variant = "primary", size = "md", loading, children, ...rest }: ButtonProps) {
  return (
    <button className={"btn btn--" + variant + " btn--" + size}
            disabled={loading || rest.disabled} {...rest}>
      {loading ? "…" : children}
    </button>
  );
}

Principle 3: composition over flags and inheritance

When a component gains a new prop for every new need (showIcon, hasFooter, footerAlign…), it is turning into a god component. The fix is to expose its internal pieces and let the consumer arrange them.

js
// Flag soup
<Card title="Plan" showFooter footerAlign="right" footerText="Buy" />

// Composition
<Card>
  <Card.Header>Plan</Card.Header>
  <Card.Body>…</Card.Body>
  <Card.Footer><Button>Buy</Button></Card.Footer>
</Card>

In React you almost never need class inheritance; composition with children, compound components and custom hooks cover every need. It is also the approach the official React docs recommend as the default.

Colourful abstract illustration of geometric brick-like pieces assembled into a user interface
Small composable components can be reused many times in many ways; a giant component only once.

Principle 4: keep state close

Define state in the lowest component that has all its consumers below it. Lifting state without reason causes large subtrees to re-render and leads to prop drilling. A simple two-step decision:

  1. If only one component needs it, keep it there (such as whether a menu is open).
  2. If several siblings need it, lift it to the nearest common parent; if the depth gets large, use Context or a state library.

Principles 5 to 7: accessibility, testing and stability

Accessibility from the first version

A custom button built from a div is repeated everywhere and so is its fix. That is why base components (button, field, modal) must ship with semantic HTML, labels and focus handling from the start; the full list is in our web accessibility checklist.

Testability

A component tested through its observable behaviour (text, role, events) will not break on internal refactors. Use Testing Library and query by role and text rather than CSS class names.

Stability and performance

Measure before reaching for React.memo and useMemo. Most speed problems come from needless re-rendering of large subtrees or heavy images, not from the component itself; see page load speed optimization for the bigger picture. If you want a design system and component library for your product, our web development team is ready to help.

Frequently asked questions

How large should a component be?

There is no exact numeric rule, but if a file passes roughly 150 lines or you cannot describe its responsibility in one sentence, it is usually time to split it.

What is the difference between controlled and uncontrolled components?

A controlled component receives its value through props and the parent manages it; an uncontrolled one keeps its own internal state. Good libraries support both.

When should I use Context?

For data that changes rarely and is needed deep in the tree, such as theme, language or the current user. For frequently updating data, Context can trigger many re-renders.

Should I create a component for every small piece?

No. Extraction needs a reason: repetition, complexity, or the need for independent testing and naming. Over-splitting also hurts readability.

Comments

    No comments yet. Be the first to share your thoughts.

    Comments are published after review.