Engineering

Small Components, Clear Boundaries

Shaquib Imdad
3 min read0 views
Sculptural pale concrete architecture against a cobalt blue sky

A component can be short and still be difficult to understand. The problem is often not its size, but the number of decisions it owns.

Imagine a story card that fetches an article, formats its date, manages a bookmark, opens a dialog, and decides which layout to use. Each responsibility might be reasonable on its own. Together, they make even a small change harder to reason about.

This guide uses a story card to explore a more useful question: where should one responsibility end and another begin?

Start with a responsibility

Before extracting a component, describe its job in one sentence. “Present a preview of an article” is a clear responsibility. “Handle everything related to articles” leaves the boundary open-ended.

A presentational story card might receive a title, description, URL, and category. It does not need to know where those values came from. A page can load the data and decide which stories belong together.

That separation gives the page control over the collection while keeping the card focused on its presentation.

Make the contract easy to read

A component’s inputs should explain what it needs. Prefer a small, explicit contract to passing an entire application object and letting the component search through it.

CODE
type StoryCardProps = {
  title: string;
  description: string;
  href: string;
  category: string;
};
 
function StoryCard(props: StoryCardProps) {
  return (
    <article>
      <span>{props.category}</span>
      <h2><a href={props.href}>{props.title}</a></h2>
      <p>{props.description}</p>
    </article>
  );
}

This is a starting point, not a universal template. A featured story may need an image and a different heading level. Add those decisions deliberately when the design calls for them.

Keep state near the decision

State belongs where the relevant decision is made. A filter that affects the whole story collection belongs above the individual cards. A local disclosure can usually stay with the control that opens it.

Ask two questions before moving state:

  • Which parts of the interface need to read this value?
  • Which part of the interface is responsible for changing it?

The answers help identify a useful owner without making every value global.

Extract when a pattern becomes clear

Two similar blocks do not always need the same abstraction. Their differences may reveal that they serve different purposes.

A chapter link jumps within a story. A story card opens a new article. They can share typography or spacing tokens while remaining separate components with distinct behavior.

Reuse a clear idea, not just a repeated arrangement of elements.

A review checklist

Before calling a component finished, check its boundaries:

  • Can its responsibility be explained in one sentence?
  • Are its inputs understandable without reading the parent?
  • Does it own only the state it needs?
  • Does its semantic HTML match its purpose?
  • Can one common change be made without touching unrelated behavior?

The goal is not the smallest possible file. It is a component whose decisions are easy to follow—and whose boundaries make the next change easier.

KEEP THE THREAD GOING

Up next.