Saved
Foundation

Component-Driven Architecture

Understanding the fundamental shift from page-based to component-based thinking in modern frontend development.

components
architecture
design-patterns
Read
14 mins

By Den Odell

Deep Dive
TL;DR

TL;DR

The web evolved from documents to applications, and our mental model must evolve with it. Component-driven architecture inverts page-based thinking—building from reusable, self-contained units rather than top-down pages.

Core principles:

Think bottom-up: build from components, not pages
Clear boundaries: encapsulate structure, behavior, and styling
Composition over configuration: combine simple parts rather than configure complex ones
Minimal knowledge: components should know as little as possible about the outside world

The art of programming is the art of organizing complexity, of mastering multitude and avoiding its bastard chaos as effectively as possible.

— Edsger W. Dijkstra

The Shift from Documents to Applications

The web started with documents. HTML was designed to mark up text with headings, paragraphs, lists, and links. Pages linked to other pages. The browser was a document viewer, and we were all publishers.

That mental model served us well for a surprisingly long time. We built websites as collections of pages, each one a distinct document with its own URL, its own content, its own place in the sitemap. Templates helped us share headers and footers across pages, but the fundamental unit of construction remained the page itself.

Then applications happened.

The shift wasn’t sudden. Gmail in 2004 hinted at what was possible. Single-page applications emerged. React landed in 2013 and changed everything—not because it was the first component library, but because it made component thinking accessible and practical at scale. Today, whether you use React, Vue, Svelte, Angular, or web components, you’re working within a component paradigm that would be unrecognisable to a developer from 2005.

This isn't just a change in tools. It's a change in how we think about building for the web.

The Page Mindset

Understanding where we came from helps us appreciate where we are.

In page-based development, you think top-down. You start with the page—its purpose, its layout, its content—and work downward into the details. Each page is a destination, a complete unit that stands alone. The question you ask is: “What does this page need to do?”

Templates in this model are about code reuse, not abstraction. You extract the header into a partial because you don’t want to repeat yourself, not because you’re modelling a “Header” concept that exists independently of any particular page. The header partial exists to serve pages. Pages don’t exist to compose headers.

Server-side frameworks reinforced this thinking. Rails, Django, PHP—they all organised code around routes and views. The URL determined which controller ran, which template rendered, which page the user saw. Everything flowed from the page.

This model has real virtues. Pages map cleanly to URLs, which means they map cleanly to how users think about navigation. They’re easy to reason about in isolation. They match how designers often work, delivering page-by-page mockups. And they align with how the web actually works at the protocol level—you request a URL, you get a document.

But the page mindset struggles with complexity. As interfaces become more interactive, as the same UI elements appear in different contexts, as applications need to feel responsive rather than document-like, thinking in pages becomes a limitation rather than a foundation.

The Component Mindset

Component-driven architecture inverts the mental model. Instead of thinking top-down from pages to parts, you think bottom-up from parts to compositions.

A component is a self-contained unit of interface that encapsulates its own structure, behaviour, and often its own styling. It has a clear boundary. It receives inputs (props, attributes, slots) and produces outputs (rendered UI, events, callbacks). It can be understood in isolation and tested in isolation. It can be composed with other components to build larger structures.

The question you ask changes. Not “what does this page need?” but “what are the building blocks, and how do they compose?”

This shift is more profound than it first appears. When you think in components, you stop seeing a navigation bar as 'the thing at the top of the page' and start seeing it as an independent entity that happens to appear at the top of pages.

The navigation bar has its own concerns: its own state (which item is active), its own logic (what happens on mobile), its own API (what links does it display). It exists conceptually before any page uses it.

Components are compositions, not templates. A template says “here’s a hole where content goes.” A component says “here’s a self-sufficient unit that can contain other self-sufficient units.” The difference is autonomy. Template partials serve their parent. Components serve their purpose.

This autonomy enables something powerful: thinking at different levels of abstraction simultaneously. A button is a component. A card containing that button is a component. A list of those cards is a component. A page composing that list with a header and sidebar is a component. Each level is complete in itself while participating in larger wholes.

The Atomic Metaphor

Brad Frost’s Atomic Design gave us a useful vocabulary for this hierarchy: atoms, molecules, organisms, templates, pages. The metaphor borrows from chemistry—small things combine into larger things according to predictable rules.

The Atomic Hierarchy

Small to Medium

Atoms are irreducible at this level of abstraction—buttons, inputs, labels. Fundamental building blocks.

Molecules are simple combinations of atoms working together—a search form combining an input and a button.

Organisms are more complex compositions that form distinct sections—a header containing logo, navigation, and search.

Medium to Large

Templates define page-level structure with placeholder content—the layout without the data.

Pages are templates populated with real content—the final instantiation users see.

Each level has different concerns and different rates of change. Atoms are stable; pages are volatile.

The metaphor is imperfect—chemistry has fixed rules while component composition is arbitrary—but it captures something true. There’s a hierarchy. Components at each level have different concerns and different rates of change. Atoms are stable; pages are volatile. You design from the bottom up but use from the top down.

What matters isn’t the specific terminology but the underlying insight: complex interfaces are compositions of simpler parts. This is true whether you call them atoms and molecules, primitives and compounds, or just “small components and big components.” The hierarchy exists regardless of what you name it.

Boundaries and Contracts

Components need boundaries. Without clear boundaries, components bleed into each other, and the benefits of encapsulation disappear.

A component’s boundary is the membrane between inside and outside. Inside the boundary, the component has full control—over its structure, its state, its implementation details. Outside the boundary, other components interact with it only through defined interfaces. They cannot reach inside. They shouldn’t need to.

This boundary is enforced by a contract: the component’s API. Props define what data flows in. Events define what signals flow out. Slots define where child content can be injected. The contract is a promise: “Give me these inputs, and I’ll behave in these ways.”

Good contracts are narrow. Every additional prop is additional coupling. Every piece of context the component requires is a dependency that makes it harder to reuse and harder to test.

The principle here is minimal knowledge. Components should know as little as possible about the world beyond their boundary. When a component needs something, prefer passing it in explicitly over having the component reach out to get it. Explicit dependencies are visible, testable, and controllable. Implicit dependencies hide complexity and create fragility.

A button component needs to know its label and what to do when clicked. It doesn’t need to know what page it’s on, what user is logged in, or what colour scheme the application uses.

This doesn’t mean components can never access global state or context—sometimes that’s the right choice. But it means such access should be deliberate, visible, and justified. The default should be self-sufficiency.

Composition Over Configuration

How should components handle variation? When you need a button that’s sometimes primary and sometimes secondary, sometimes large and sometimes small, sometimes with an icon and sometimes without—how do you model that?

Two Approaches to Variation

Configuration Approach

Add props for every variant:

<Button
  variant="primary"
  size="large"
  icon="save"
  iconPosition="left"
>
  Save
</Button>

The component becomes a Swiss Army knife, handling every possible case through a growing API.

Composition Approach

Build larger components from smaller ones:

<PrimaryButton>
  <SaveIcon />
  Save
</PrimaryButton>

Variation comes from combining different building blocks rather than configuring a single complex block.

Composition scales better than configuration. Configuration props multiply combinatorially—if you have three sizes and four variants and two icon positions, you have twenty-four combinations to design, implement, and test within a single component. Composition keeps complexity local—each building block handles its own concerns, and combinations emerge naturally.

Composition also handles unforeseen cases. A heavily configured component can only do what its author anticipated. A compositional approach lets consumers build combinations the component author never imagined. Flexibility comes from the architecture, not from feature flags.

The compound component pattern exemplifies this thinking. Instead of a select component that takes an array of options as a prop, you have a Select that composes Option children. Instead of a tabs component configured with an array of tab definitions, you have Tabs composing Tab and TabPanel children. The parent provides context; the children provide content. The API is the composition itself.

Slots and children extend this further. A modal component doesn’t need header, body, and footer props—it can accept children and render them in the right places. A card doesn’t need title and subtitle and actions props—it can accept CardHeader, CardBody, and CardFooter children. The component defines the structure; consumers fill in the substance.

This doesn't mean props are bad. Simple, stable variations often belong as props. The principle is: when variation grows, reach for composition first. Don't let a component become a configuration monster.

State and Behaviour

Components need to manage state—but how much state, and of what kind?

Local state is state that belongs to a single component instance. A dropdown’s open/closed status. An input’s current value before form submission. A counter’s count. An accordion’s expanded sections. This state is internal to the component, invisible to the outside world, and managed entirely within the component’s boundary.

Local state is good. It’s encapsulated, testable, and doesn’t create coupling. A component that manages its own local state is self-sufficient. Changes to that state don’t ripple through the application. You can drop that component anywhere, and it just works.

Derived state is state calculated from other state. A filtered list derived from a full list and a search term. A total derived from line items. These values don’t need their own storage—they’re computed on demand from the source data. Storing derived state separately creates synchronisation bugs; computing it keeps the source of truth singular.

The challenge comes when state needs to be shared. When two components need to react to the same data, that data has to live somewhere both can access. This is where the component mindset meets the reality of application architecture, and where many teams struggle.

The temptation is to lift state high—put it at the root, make it available everywhere. This works but creates hidden dependencies. Components that look self-contained actually depend on global state structures. Change the shape of that state, and you break components scattered across your codebase.

The component-driven principle is: state should live as close as possible to where it's used. Only lift state when necessary for sharing, and only to the nearest common ancestor of the components that need it.

Global state is sometimes necessary, but it’s a cost, not a convenience.

Server state—data that comes from and belongs to the server—follows different rules. It’s cached locally, yes, but the source of truth is elsewhere. Modern data-fetching patterns recognise this distinction, treating server state as a cache to be synchronised rather than application state to be managed. This separation clarifies thinking: What’s truly local? What’s shared within this client? What belongs to the server?

Thinking in Components

Adopting component-driven architecture isn’t just about using a component framework. It’s about changing how you see interfaces.

When you look at a design mockup, do you see pages or building blocks? When you plan a feature, do you think about what screens to build or what components to create? When you review code, do you ask whether the page works or whether each component is coherent?

The component mindset sees every interface as a composition. A complex screen isn’t a monolithic thing to build; it’s a puzzle to decompose. The challenge isn’t implementing the screen—it’s identifying the right components, the right boundaries, the right contracts.

This decomposition is design work, even if it doesn’t look like traditional design. Deciding where component boundaries fall, how components communicate, what contracts they expose—these decisions shape the system’s long-term maintainability more than almost any other factor.

Good decomposition is fractal: you can zoom in to any component and find it composed of smaller components, each with their own clarity and purpose. Bad decomposition creates components that are either too granular (doing almost nothing, requiring many to accomplish anything) or too monolithic (doing everything, with tangled internal structure).

Questions That Reveal Problems

Finding the right granularity is an art learned through practice. But the questions to ask are consistent:

  • Does this component have a single, clear purpose? Can I explain what it does in one sentence?
  • Could someone use it without understanding its implementation? Is the API self-evident?
  • Would I be comfortable publishing it as a library? Is it truly self-contained?

The answers reveal problems. If explaining a component’s purpose requires “and” (“it displays user data and handles form submission and manages the modal state”), the component is doing too much. If testing a component requires mocking half the application, its dependencies are too broad. If changing a component’s styling breaks its behaviour, its concerns aren’t separated.

Refactoring in component-driven development often means decomposition. The fix for a component that’s too complex isn’t usually making it simpler—it’s splitting it into multiple components, each with simpler responsibilities. What was one tangled thing becomes several clear things composed together.

Components Across Frameworks

One beauty of the component mental model is its portability. React, Vue, Svelte, Angular, Solid, web components—they all work with components. The syntax differs, the reactivity models differ, the specific patterns differ. But the underlying way of thinking transfers.

A React developer thinking in components can understand a Vue codebase conceptually, even if the code looks unfamiliar. A Svelte developer can sketch component boundaries on a whiteboard in a way that a React developer immediately recognises. The framework is an implementation detail; the component model is the architecture.

This portability is valuable in a world where frameworks rise and fall, where teams get acquired and merge codebases, where developers change jobs and encounter new stacks. Thinking in components is a transferable skill. Knowing a specific framework’s syntax is necessary but not sufficient.

Beyond the Browser

Component thinking has spread beyond frontend development. Design tools like Figma now have component systems. Backend development increasingly uses component-like patterns for UI rendering. Mobile development has adopted similar compositional models.

This convergence isn’t coincidence. Component architecture solves a universal problem: how do you build complex things from simpler things while maintaining coherence? The answer—encapsulated units with clear boundaries and explicit contracts that compose into larger wholes—applies wherever you’re building interfaces.

The page didn't disappear. But the page has been demoted from the fundamental unit of construction to an assembly point—a place where components come together to serve a purpose.

The page exists because users need destinations. Components exist because builders need building blocks.

Summary

Component-driven architecture represents a fundamental shift in how we think about building for the web:

  1. Think bottom-up from building blocks to compositions, not top-down from pages to parts
  2. Encapsulate completely with clear boundaries between component internals and external interfaces
  3. Compose over configure to handle variation through combination rather than configuration
  4. Keep state local and lift only when necessary for sharing
  5. Design at every level because component boundaries are architectural decisions that shape maintainability
  6. Transfer thinking across tools because the component mindset transcends any particular framework

The page-based web served us well, but modern interfaces demand a different mental model. Component-driven architecture provides that model—not as a framework feature, but as a way of thinking about how complex interfaces are built, maintained, and evolved.


Related Content:

External Resources:

Stay Updated

Get New Patterns
in Your Inbox

Join thousands of developers receiving regular insights on frontend architecture patterns

No spam. Unsubscribe anytime.