Lego, Not Sculpture: Thinking in Components
Splitting an interface into pieces looks like a formatting decision and is actually the design of a small language your team will speak for years.
Two developers build the same screen.
The first writes it as one file: the header, the filters, the table, the row actions, the pagination, the empty state. It works, it's five hundred lines, and every future change means reading all five hundred to find where.
The second writes a page that reads like a sentence — header, filter bar, table, pagination — each of those a piece defined elsewhere, each understandable on its own. It also works. The page file is thirty lines.
The difference isn't tidiness. It's that the second developer can change the filter bar without reading the table, test the empty state without producing an empty database, and give the pagination to another project on Tuesday. Everything below is about how to make the second version happen on purpose.
A component is a function from data to interface
Strip away syntax and a component is a function: it takes data — props — and returns a description of what should be on screen. Same props in, same output out.
That framing answers most beginner questions. Why can't a component change its own props? Because a function doesn't rewrite its arguments; that's the caller's data. Where does data come from? From above, passed in. How does a child affect its parent? By calling a function the parent gave it.
Which is the flow the whole model rests on: data goes down, events go up. A change happens in one place — the component that owns that state — and everything below it re-renders from the new value. Two-way binding between arbitrary components is what you're avoiding, and it's worth knowing why: when anything can change anything, "who set this value?" becomes unanswerable, and it's unanswerable exactly when you need the answer.
Where to draw the lines
The instruction "split it into components" is useless without a criterion, and beginners apply it in both wrong directions — one enormous component, or forty components where a button is wrapped in a wrapper wrapped in a container.
Four signals that something deserves to be its own component:
- It repeats. The same visual thing appears twice or more.
- It's independently comprehensible. You can describe what it is in one sentence without mentioning its surroundings.
- It owns state. It has an open/closed, a current tab, a draft value.
- It's replaceable. You could swap the implementation without the parent noticing.
And a signal in the other direction: if extracting it means passing eight props that only exist to reconstruct the context it just left, the seam is in the wrong place. Every extraction costs a jump for the reader; make it pay for itself.
The usable rule of thumb is the working memory limit from the How to Learn sphere: if you can't hold what a component does in your head at once, it's too big. That's not a line count — it's a count of independent things happening. A hundred lines of straightforward markup is fine; forty lines juggling four unrelated concerns is not.
Composition beats configuration
Here's the most common way component design goes wrong, and it happens gradually.
You write a card. Then someone needs one with an image, so you add a prop. Then one without a border, one with a footer, one where the title is a link, one with an icon, one in a compact size. Six months later the card takes fourteen props, nine of them booleans, and its body is a thicket of conditionals nobody will refactor.
The alternative is to let the caller pass content in rather than describe it: children, slots, named regions. Instead of hasFooter and footerText and footerAlign, the card accepts a footer — and the caller puts whatever they want in it. The card stops needing to know about every case, because it no longer decides them.
The rule: props for data and a small number of variants; composition for structure. When you catch yourself adding the fourth boolean, that's the signal — not to add a fifth, but to open a slot.
Props are an API
Naming and prop design deserve more care than they usually get, because you're not writing an implementation detail: you're designing a small language other people — including you in three weeks — will use without reading the source.
A few things that consistently pay off. Name for meaning, not appearance: variant="danger" survives a redesign, variant="red" doesn't. Prefer a single status prop over three mutually-exclusive booleans, which allow illegal combinations to be expressed. Give sensible defaults, so the common case is short. And keep the surface small — every prop is a promise to keep working.
The smell to watch for is prop drilling: a value passed through four components that don't use it, just to reach the fifth. Once or twice it's fine and simpler than the alternative. Repeatedly, it means that value is really shared state and belongs in a context or a store — which is the next article's subject.
A component's props are an API. Its main user is you, three weeks from now, having forgotten everything.
Lists and keys
Every framework needs a way to know which item is which when a list changes, and it's the source of a famously confusing bug class.
Give each item a stable identity — an id from your data — and the framework can tell that an item moved rather than assuming its contents changed. Use the array index instead, and reordering, inserting or deleting makes the framework match up the wrong items: state attached to a row follows the wrong row, an input keeps text from a different item, a checkbox appears to tick something you never touched.
The rule is short: a stable id from the data, never the index, unless the list is fixed and never reorders.
Reusable isn't automatically good
There's a bias in this field towards abstracting early, and it costs more than duplication does.
The problem: you extract a shared component after seeing two similar cases, and then case three is nearly the same, so it gets a flag. Case four gets another. Soon the shared component is a conditional maze serving four things that were never really the same, and every change to one risks the other three.
Two heuristics that hold up. Wait for the third occurrence before abstracting — two is a coincidence, three is a pattern with visible shape. And duplication is cheaper than the wrong abstraction: copied code is easy to delete and easy to change independently; a wrong abstraction is a knot that gets tighter with every case.
The other side of the design system
If your project has a design system, its components are the same object the UX sphere's design-systems article describes, seen from the code side. The tokens are the shared values; the components are the shared decisions.
Which makes a component library an agreement rather than a folder: what a button is, which variants exist, what a card contains. When it works, a designer's "use the secondary button" and your variant="secondary" are the same sentence — and the entire class of "make it look like the mockup" work disappears.
Building components in isolation — in a component workbench, or just on a scratch page rendering every state side by side — is what makes this practical. You see all the states at once, including the ones that are hard to reach in the real application.
In practice
Split an existing page on paper before coding. Draw boxes and name each one. If a box needs "and" to describe it, it's two boxes.
Build one component with an intentionally bad API, then rebuild it well. Ten booleans versus slots, on the same card. Feeling both is worth more than a rule you agreed with — that's practising at the edge rather than repeating what you can already do.
Count the booleans. Four is the point at which you open a slot instead.
Render every state side by side on a scratch page: empty, loading, error, one item, many, long text, missing image. Five minutes, and it finds the cases that reach production otherwise.
Grow a personal library. After a few months you should have your own versions of the recurring pieces — button, field, modal, table, empty state, toast. These are your chunks in this stage, and they're built the same way all chunks are: by making each several times, in different contexts, with the result checked.
Reread a component you wrote a month ago and try to use it without opening the body. If you can't, the API is the thing that needs fixing, not your memory.
Check yourself
Close the article and answer in your own words:
- Why can't a component change its own props, and what replaces that?
- Give four signals that something should become a component, and one that it shouldn't.
- What's the practical difference between configuring with props and composing with slots?
- Why is
variant="danger"better thanvariant="red"? - What is prop drilling, and when does it stop being acceptable?
- What breaks when you use an array index as a key?
- Why is duplication sometimes cheaper than a shared component?
In short
- A component is a function from props to interface; data flows down and events flow up, which is what keeps "who changed this?" answerable.
- Split where something repeats, is independently describable, owns state, or is replaceable — and stop when a component no longer fits in your head at once.
- Configure with props for data and variants; compose with slots for structure, and treat the fourth boolean as the signal to switch.
- Props are a public API: name for meaning, prefer one status over several booleans, give defaults, keep the surface small.
- Prop drilling through several layers is a sign the value is shared state, not a prop.
- Keys must be stable ids from the data; indexes make the framework match up the wrong items when a list changes.
- Abstract on the third occurrence, and remember that duplication is cheaper than a wrong abstraction.
- Components are the code half of a design system, and building them in isolation with every state visible is what makes the agreement real.