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.
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.
