Module 1: Thinking In Components
Decomposing the storefront into components
Overview
So far we've worked with one component: ProductCard. But a real screen isn't a card; it's a whole store —search bar, product list, cart—. The question of this lesson is the one you face every time you start a new interface: how do I split this screen into components? Where do the boundaries go? What's a piece and what's part of another?
Decomposing isn't cutting at random. It's an act of design with clear criteria, and this lesson gives them to you. The underlying idea is that each component should have one responsibility: one thing it does and that it owns. ProductCard owns "showing a product"; SearchBar owns "the search bar"; CartItem owns "one cart line". When a piece starts doing two things —showing a product and managing the cart and filtering the list—, it's a sign that there are two components disguised as one.
You're going to learn to look at a design and see the components hidden in it, guided by two concrete clues —repetition (something that appears many times is usually a reusable component) and single responsibility (each piece, one job)—, to trace the boundaries, and to draw the resulting component tree. And we'll verify it by running each piece separately, to see that a well-delimited component is a self-contained unit that produces its own fragment of HTML without depending on the others.
Connection with the module. Decomposing is the bridge between "I know what a component is" (lessons 2 and 3) and "I know how to build an app" (lessons 5 to 8). Once you have the pieces well delimited, composition (lesson 5) assembles them, the props (lesson 6) pass them their data, and the reasons to decompose (lesson 7) justify why you took the trouble. This is the step that "Thinking in React" —the canonical essay of the official documentation— puts as the very first of all: start by breaking the UI into a hierarchy of components. Here we do it with Mercado's storefront.
An analogy: modular furniture
Think about how you furnish a kitchen with modular furniture (like the ones from a Swedish furniture store). You don't buy "a kitchen" as a single giant piece; you buy modules: a drawer module, a door module, a shelf, a rack. Each module does one thing —store cutlery, hang, hold plates— and you combine them to build the kitchen you need.
Why are they sold like this and not as a single piece? For three reasons that are, exactly, those of decomposing a UI.
Because they repeat. You use the same drawer module three times along the wall. They didn't design three different drawer units: they made one and you repeat it. In a UI, the product card repeats a hundred times in the catalog: you don't make a hundred cards, you make one ProductCard and repeat it.
Because each one has a job. The drawer module stores; the shelf holds. If a module tried to store and hold and hang and also be the sink, it would be a monster impossible to manufacture, move, or replace. Each module, one function. Each component, one responsibility.
Because they're replaced without touching the rest. If the drawer module gets damaged, you change it alone, without disassembling the kitchen. If a component has a bug, you fix it alone, without touching the others.
The act of "decomposing" is, exactly, looking at the kitchen you want and deciding which modules make it up and where each one goes. Looking at Mercado's storefront and deciding its components is the same act. Keep the modular furniture; it's decomposition in one image.
How to look at a screen and see its components
Let's go back to Mercado's storefront design and do the exercise of decomposing it, out loud, with criteria.
┌──────────────────────────────────────────────┐
│ [ Search products... ] │ <- 1
├──────────────────────────────────────────────┤
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ <- 2 (contains 3)
│ │Wireless │ │Mechanical │ │USB-C Hub │ │
│ │Mouse │ │Keyboard │ │ │ │ <- 3 (repeats)
│ │$25.99 │ │$89.00 │ │$34.99 │ │
│ │In stock │ │Out of stock│ │In stock │ │
│ └────────────┘ └────────────┘ └────────────┘ │
├──────────────────────────────────────────────┤
│ Your cart │ <- 4 (contains 5)
│ • Wireless Mouse x1 │ <- 5 (repeats)
│ • USB-C Hub x2 │
└──────────────────────────────────────────────┘
We apply two clues. Clue 1: what repeats is a component. Look at the product cards: they're the same structure (name, price, stock) repeated three times with different data. That screams "reusable component": it's ProductCard. Same with the cart lines (name + quantity, repeated): it's CartItem. Clue 2: each zone with a clear responsibility is a component. The search bar does one thing (search): it's SearchBar. The zone that groups all the cards does one thing (list products): it's ProductList. The cart zone does one thing (show the cart): it's Cart. And something has to contain the three: it's App.
Notice the containment hierarchy, which is what forms the tree. ProductList contains the ProductCards (zone 2 contains the zones 3). Cart contains the CartItems (zone 4 contains the zones 5). And App contains SearchBar, ProductList, and Cart. That "X contains Y" relationship is exactly the parent-child relationship of the component tree:
flowchart TD
App[App] --> SearchBar[SearchBar]
App --> ProductList[ProductList]
App --> Cart[Cart]
ProductList --> ProductCard[ProductCard]
Cart --> CartItem[CartItem]
(We draw a single ProductCard and a single CartItem to avoid repetition; in practice there's one per product and per cart line, as we saw in lesson 1.)
Note a design decision: we could have made ProductList a single giant piece that drew all the cards internally, without ProductCard. It would be a mistake. By separating ProductCard, we gain a reusable piece (the card), with a clear responsibility (showing one product), that we can test alone and use elsewhere (lesson 7 takes advantage of this). The rule: when you see a repetition or a separable responsibility, pull it out into its own component.
Worked example: each piece, separately
We're going to verify the most valuable property of a good decomposition: each component is a self-contained unit. If they're well delimited, I can take SearchBar, ProductCard, or CartItem and render each one alone, without the others, and each produces its own correct fragment of HTML. That independence is the proof that the boundaries are well placed.
First, the three components in JSX (the real API):
function SearchBar(props) {
return (
<form className="search-bar">
<input className="search-input" value={props.query} placeholder="Search products" />
</form>
);
}
function ProductCard(props) {
return (
<article className="product-card">
<h3 className="product-name">{props.name}</h3>
<p className="product-price">{formatPrice(props.priceCents)}</p>
<span className="product-stock">{props.inStock ? 'In stock' : 'Out of stock'}</span>
</article>
);
}
function CartItem(props) {
return (
<li className="cart-item">
<span className="cart-item-name">{props.name}</span>
<span className="cart-item-qty">{'x' + props.quantity}</span>
</li>
);
}
Here something new appears: SearchBar's <input>. In HTML, input is a void element: it has no content or closing tag (there's no </input>). Our renderToString until now assumed that every element closes, so we extend it by one line to handle void elements (input, img, br, hr), which render as <input ... /> without closing. It's the kind of small, motivated extension you'll make in real code. Here's the executable version, with that extension:
function h(tag, props, ...children) {
return { tag, props: props || {}, children: children.flat() };
}
const VOID = new Set(['input', 'img', 'br', 'hr']);
function renderToString(node, indent = 0) {
const pad = ' '.repeat(indent);
if (node == null || typeof node === 'boolean') return '';
if (typeof node !== 'object') return pad + node; // text
const attrs = Object.entries(node.props)
.map(([k, v]) => ` ${k === 'className' ? 'class' : k}="${v}"`).join('');
if (VOID.has(node.tag)) return `${pad}<${node.tag}${attrs} />`; // input, img: no closing
const kids = node.children.filter((c) => c != null && c !== false);
if (kids.length <= 1 && kids.every((c) => typeof c !== 'object'))
return `${pad}<${node.tag}${attrs}>${kids[0] ?? ''}</${node.tag}>`;
const inner = kids.map((c) => renderToString(c, indent + 1)).join('\n');
return `${pad}<${node.tag}${attrs}>\n${inner}\n${pad}</${node.tag}>`;
}
function formatPrice(cents) {
return '$' + (cents / 100).toFixed(2);
}
// Each piece of the screen is its own component, and renders alone.
function SearchBar(props) {
return h('form', { className: 'search-bar' },
h('input', { className: 'search-input', value: props.query, placeholder: 'Search products' })
);
}
function ProductCard(props) {
return h('article', { className: 'product-card' },
h('h3', { className: 'product-name' }, props.name),
h('p', { className: 'product-price' }, formatPrice(props.priceCents)),
h('span', { className: 'product-stock' },
props.inStock ? 'In stock' : 'Out of stock')
);
}
function CartItem(props) {
return h('li', { className: 'cart-item' },
h('span', { className: 'cart-item-name' }, props.name),
h('span', { className: 'cart-item-qty' }, 'x' + props.quantity)
);
}
console.log('--- SearchBar alone ---');
console.log(renderToString(SearchBar({ query: 'mouse' })));
console.log('\n--- ProductCard alone ---');
console.log(renderToString(ProductCard({ name: 'USB-C Hub', priceCents: 3499, inStock: true })));
console.log('\n--- CartItem alone ---');
console.log(renderToString(CartItem({ name: 'Wireless Mouse', quantity: 2 })));
What to expect. When you run the file, the output is exactly this:
--- SearchBar alone ---
<form class="search-bar">
<input class="search-input" value="mouse" placeholder="Search products" />
</form>
--- ProductCard alone ---
<article class="product-card">
<h3 class="product-name">USB-C Hub</h3>
<p class="product-price">$34.99</p>
<span class="product-stock">In stock</span>
</article>
--- CartItem alone ---
<li class="cart-item">
<span class="cart-item-name">Wireless Mouse</span>
<span class="cart-item-qty">x2</span>
</li>
What matters about this output isn't each fragment by itself —you already know how to read HTML—, but that each component rendered alone, without the others. We could take SearchBar and get its <form> with its <input> without any list or any cart existing. We could take ProductCard and get its card without the search bar existing. We could take CartItem and get its <li> without the cart that contains it existing. Each piece is self-contained: it receives its props, produces its fragment, and needs nothing from the others.
That independence is the proof of a good decomposition, and it has very concrete consequences. It means you can develop ProductCard without having built ProductList yet. You can test CartItem with fake data, in isolation, without spinning up the whole app. You can reason about SearchBar by looking only at its function, without carrying the whole storefront in your head. If, on the contrary, ProductCard couldn't render without the cart —if it depended on it—, the boundaries would be badly placed: you'd have cut where you shouldn't have.
Notice also the <input ... /> in SearchBar's output: thanks to the renderToString extension, it came out as a void element, without </input>. A small detail, but it's exactly the kind of thing that separates "code that looks like HTML" from "correct HTML".
Common mistakes
The giant component that does everything. What happens: instead of decomposing, someone writes a single two-hundred-line Storefront that draws the bar, assembles the list product by product, and paints the cart, all internally. Why it happens: at first it seems faster not to "bother" creating pieces. How to spot it: your component does more than one thing; to understand one piece you have to read the whole thing; you can't reuse any part because everything is intertwined. How to fix it: apply single responsibility. Each zone with its own job —the search, the list, a card, the cart, a line— goes out to its own component. The alarm signal is the conjunction "and": "this component shows the list and the cart and the search" means there are three components there. A component that needs "and" to describe itself is doing too much.
Cutting in the wrong place (boundaries that don't fit the responsibility). What happens: components are created that don't correspond to a real responsibility, like TopHalf and BottomHalf, splitting the screen by geometry instead of by function. Why it happens: "dividing the visual space" is confused with "dividing responsibilities". How to spot it: a component groups things that have nothing to do with each other (half the search and half the list), or a responsibility ends up split between two components. How to fix it: cut by responsibility, not by position. The question isn't "what's on top and what's on the bottom?", but "what does this piece do?". SearchBar is a responsibility; "the top half of the screen" is not.
Over-decomposing: a piece for every little thing. What happens: at the opposite extreme, someone creates ProductName, ProductPrice, ProductStockLabel as separate components, when they're just three lines inside ProductCard. Why it happens: "decompose" is applied without measure. How to spot it: you have one-line components used in only one place that add neither reuse nor clarity; you jump between twenty files to understand one card. How to fix it: decompose when there's repetition (it's used in several places) or when a responsibility is big enough to deserve its own space. Three lines that always go together and only here don't need to be three components; they're fine as part of ProductCard. The criterion is balance: neither a monster that does everything, nor a swarm of trivial pieces.
Exercises
Exercise 1 — Decompose a new screen. Mercado adds a "seller profile" page with: a header (photo, seller name, rating), a list of their products (each with name and price), and a reviews section (each review with author and text). Propose the components you'd decompose it into and draw their tree. Justify each component with one of the two clues (repetition or responsibility).
See solution
A reasonable decomposition:
SellerProfile
├── SellerHeader (responsibility: show the seller's identity)
├── SellerProductList (responsibility: list the seller's products)
│ └── ProductCard (repetition: one card per product -> reusable)
└── ReviewList (responsibility: show the reviews)
└── ReviewCard (repetition: one card per review)
Justification:
SellerHeader: single responsibility (the seller's identity: photo, name, rating). It doesn't repeat, but it's a zone with a clear job.SellerProductListandReviewList: each has a responsibility (list products / list reviews) and contains repeated pieces.ProductCard(the same one from Mercado, reused!) andReviewCard: repetition —one card per product, one per review—, so they go out to their own component.SellerProfile: the root container, analogous toApp.
The valuable part: you could reuse ProductCard, which already exists, for the seller's products. That's the reward of having separated it before. There's no single "correct" answer, but any good decomposition cuts by responsibility and pulls the repetitions out into reusable components.
Exercise 2 — Detect the giant component. A colleague wrote this. Explain what's wrong from the decomposition point of view and rewrite it, splitting it into components with single responsibility (you can write just the structure, with h or JSX):
function Storefront(props) {
return h('div', {},
h('form', {}, h('input', { value: props.query })),
h('section', {},
props.products.map((p) =>
h('article', {}, h('h3', {}, p.name), h('p', {}, formatPrice(p.priceCents)))
)
)
);
}
See solution
What's wrong: Storefront does three jobs at once —the search bar, the list, and each product's card— all intertwined in a single function. The card's structure (article with h3 and p) is written inside the map, so it can't be reused or tested separately; and the search bar is mixed in with the list. It's the giant component from the first common mistake.
Rewritten, split by responsibility:
function SearchBar(props) {
return h('form', { className: 'search-bar' },
h('input', { className: 'search-input', value: props.query })
);
}
function ProductCard(props) {
return h('article', { className: 'product-card' },
h('h3', { className: 'product-name' }, props.name),
h('p', { className: 'product-price' }, formatPrice(props.priceCents))
);
}
function ProductList(props) {
return h('section', { className: 'product-list' },
props.products.map((p) => ProductCard(p))
);
}
function Storefront(props) {
return h('div', { className: 'storefront' },
SearchBar({ query: props.query }),
ProductList({ products: props.products })
);
}
Now each piece has a job: SearchBar (search), ProductCard (a card —reusable, testable alone), ProductList (the list), and Storefront just assembles the three. The card's structure came out of the map into its own component, so now it can be reused. This last bit —ProductList calling ProductCard— is composition, which is exactly the topic of lesson 5.
Exercise 3 — Why could it render alone? In the worked example we rendered CartItem completely alone, without Cart or App. Explain what property of a good decomposition made that possible, and what it would have meant if CartItem couldn't render without Cart.
See solution
It could render alone because CartItem is a self-contained unit: everything it needs to produce its HTML reaches it through its props (name and quantity), and it doesn't depend on any other component to work. We passed it { name: 'Wireless Mouse', quantity: 2 } and it produced its complete <li>, without any Cart existing around it. That autonomy is the mark that the component's boundary is well placed: the piece has a clear responsibility and a well-defined input.
If CartItem couldn't render without Cart —for example, if it looked for data "upward" in the cart, or if it depended on variables that only Cart defines—, it would be a sign that the boundaries are wrong: the responsibility of "one cart line" would be tangled with that of "the cart", and the two pieces would be coupled. The consequences would be expensive: you couldn't test CartItem in isolation, you couldn't reuse it in another context, and to understand one line you'd have to carry the whole cart in your head. The independence we verified isn't a luxury: it's what makes the decomposition worth it.
Summary and next step
In this lesson you learned to decompose: look at a screen and split it into components with criteria. You saw the two clues —repetition (something that repeats is a reusable component, like ProductCard or CartItem) and single responsibility (each piece, one job, like SearchBar or Cart)— and applied them to Mercado's storefront to trace its tree (App → SearchBar + ProductList → ProductCard; App → Cart → CartItem). And you verified, by running each piece separately, the property that validates a good decomposition: each component is self-contained, produces its own fragment of HTML without depending on the others. Along the way, you extended renderToString for void elements (<input />).
Before moving on you should be able to: look at a design and propose its components, justifying each with a clue (repetition or responsibility); draw the containment tree; detect the giant component and split it; and explain why the independence of each piece is the proof that the boundaries are right.
Lesson 5 takes those separate pieces and assembles them: composition. You're going to see how ProductList doesn't draw the cards internally, but renders ProductCard for each product —components inside components—, and run the complete list to read the resulting nested HTML. It's the mechanism by which small pieces become whole screens.
Resources
- React, "Thinking in React" — react.dev/learn/thinking-in-react. Its step 1 is, literally, "break the UI into a hierarchy of components", with the same criteria as this lesson. Required reading. In English.
- React, "Your First Component" — react.dev/learn/your-first-component. Reinforces what a component is and how a piece is extracted from a larger UI. In English.
- MDN, "Void element" (Glossary) — developer.mozilla.org/en-US/docs/Glossary/Void_element. What void elements are (
input,img,br,hr) and why they don't carry a closing tag; the detail we extended inrenderToString. In English. - MDN, "
<input>: The Input element" — developer.mozilla.org/en-US/docs/Web/HTML/Element/input. Reference for the elementSearchBaruses. In English.