Front End Lesson 7 – Component-Driven Development | Dataplexa
Front End Engineering · Lesson 7

Component-Driven Development

Learn to build interfaces from the bottom up — starting with isolated, independently testable components before assembling them into pages — and see how this discipline reshapes the way you think about state, testing, and delivery speed.

The Engineer Who Couldn't See the Component

In 2017, a Facebook engineer working on their notification bell component had to spin up the full Facebook application — authentication, news feed, friends API, ads pipeline — just to see whether a small animation change on the bell icon looked right. The component was impossible to view in isolation. Its state was entangled with global application state. Its rendering depended on real API responses. Making a one-line CSS change required a ten-minute feedback loop.

That story — told internally and referenced in later writings about React's development philosophy — was part of what motivated the component model React popularised. Not just the technical abstraction, but the discipline around it: components should be independently renderable, independently testable, and independently deployable. If you need the whole application running to see a single component, you've already lost control of your codebase.

Component-Driven Development (CDD) is the practice of building UIs in that order — bottom up, component first, page last. It's not a framework. It's not a tool. It's a discipline, and like all disciplines, its value compounds over time as the codebase grows and the team scales.

What a Component Actually Is — Beneath the Abstraction

The word "component" gets used so loosely in front end engineering that it can mean almost anything — a file, a function, a design element, a piece of a page. For the discipline of CDD to make sense, the definition needs to be precise.

A component is a self-contained unit of UI with a defined interface. It accepts inputs (props), manages or receives state, renders visual output, and handles user interaction. Everything a component needs to render correctly should be expressible through its props — or through context it explicitly depends on. A component that silently reaches into global state, or that makes API calls without those calls being configurable in tests, is not truly self-contained. It's a page fragment wearing a component's clothing.

Self-Contained

The component carries everything it needs to render: markup, styles, logic, and a clear prop interface. Deleting it removes all of those things together. No orphaned CSS. No dangling event handlers. No implicit dependency on a sibling component's side effect.

Composable

Components are designed to work inside other components. A Card contains an Avatar and a Badge. A Dashboard contains a grid of Cards. The composition model makes complex UIs buildable from simple parts, and testable at each level of the hierarchy independently.

Reusable

A component built for one context should be usable in others without modification. This requires resisting the urge to hard-code specifics. A Button with a hard-coded label is not reusable. A Button that accepts children and a variant prop is. Reusability is not free — it requires deliberate API design.

Independently Testable

A component must be renderable in isolation — in a test runner, in Storybook, in a blank HTML file. If testing a component requires mocking a database, authenticating a user, or spinning up a server, the component's boundaries are wrong. Independence is not a testing convenience — it is a design signal that the component is correctly scoped.

The PixelForge Frontend team uses a simple heuristic they call the "blank canvas test": can you render any component, in any state, by passing only props — with no running backend, no authenticated session, no global store? If the answer is no for more than a handful of data-fetching container components, the architecture has drifted and needs correcting. They run this test automatically on every component through Storybook — if a story crashes because of a missing global dependency, the build fails.

The Component Hierarchy — Building Up, Not Down

The instinct when starting a new feature is to begin with the page — sketch the full layout, then figure out what parts it needs, then implement those parts. CDD inverts this. You start with the smallest, most primitive element the design requires, build it in isolation, verify it works, then compose it upward until you have the page.

Brad Frost's Atomic Design methodology — published in 2013 and still one of the most practically useful mental models for CDD — describes five levels of the hierarchy. The names come from chemistry and biology, which initially sounds affected, but the underlying structure maps cleanly onto how large component libraries are actually organised.

The Component Hierarchy — Bottom Up

1
Atoms — the indivisible primitives
The smallest useful UI elements: a Button, an Input, a Label, an Avatar, a Badge, an Icon. Each one is a single, focused visual element. At PixelForge: ColorSwatch, CursorDot, LayerIcon.
2
Molecules — atoms combined with a purpose
A SearchField (Input + Icon + Button). A UserChip (Avatar + Label + Badge). A FormField (Label + Input + ErrorText). The molecule has a single, clear responsibility. At PixelForge: CollaboratorTag, LayerRow, CanvasToolButton.
3
Organisms — meaningful, self-sufficient sections
A Navbar, a CommentThread, a ProjectCard, a LayerPanel. These are complex enough to have their own data requirements and internal state, but they don't own an entire page. At PixelForge: CanvasToolbar, CollaboratorsList, PropertiesPanel.
4
Templates — page layouts without real data
The skeleton of a page — the layout grid, the slot positions for each organism, the responsive behaviour — filled with placeholder content. A template validates that the organisms fit together before real data is wired in. At PixelForge: EditorLayout, DashboardLayout.
5
Pages — templates with real data and real routes
The final layer. Pages are thin: they wire a template to a data source and a route. They don't contain layout logic. They don't contain component logic. They are orchestrators, not authors. At PixelForge: EditorPage, ProjectDashboardPage, SettingsPage.

The practical consequence of this hierarchy is that most bugs never reach the page level. A CollaboratorTag that renders the wrong colour for offline users is caught when its story is reviewed or its unit test fails — not when a product manager notices it in production on the full editor page. Each level of the hierarchy is a checkpoint, and the earlier a bug is caught, the cheaper it is to fix.

Storybook as the CDD Development Environment

Storybook is purpose-built for component-driven development. Each component gets a dedicated story file that renders it in isolation — no router, no store, no API. Engineers develop the component entirely within Storybook, building out every meaningful state (default, loading, error, empty, hover, disabled) before the component is ever used on a page. The PixelForge Frontend team's workflow is explicit: a component does not merge until it has Storybook stories for every documented state. This means every component is visually reviewed in isolation before it reaches the page, and the story file serves as executable documentation for every future engineer who touches the component.

Props — Designing a Component API That Lasts

The props interface of a component is its public API. Once other components depend on it, changing it is a breaking change. This means prop design deserves the same level of deliberateness as any public API — and most front end teams treat it with roughly a tenth of the care they'd give an HTTP endpoint.

Bad prop APIs are the single biggest source of "component drift" — the phenomenon where a component gradually accumulates special-case props (isLargeOnMobile, showBorderOnlyInDashboard, legacyButtonStyle) until it becomes a museum of every product context it was ever hacked to support.

Pattern Example When to Use Watch Out For
Variant prop variant="primary" | "secondary" | "ghost" Visually distinct but structurally identical versions of the same component Variant explosion — 12 variants usually signals the component is doing too much
Size prop size="sm" | "md" | "lg" Proportionally scaled versions sharing the same variant and structure Avoid numeric sizes (size={24}) — they leak implementation detail into the API
Composition via children <Card><CardHeader /><CardBody /></Card> When the internal layout of the component varies significantly across contexts More flexible but requires more discipline from consumers — can be misused
Render prop / slot renderIcon={() => <StarIcon />} When a small, specific part of the component's layout needs to be customised Overusing render props creates callback soup — prefer children composition for full layouts
Boolean props isLoading isDisabled isFullWidth Simple on/off states that don't warrant a full variant Avoid more than 4–5 booleans — a component with 9 boolean props is a design system smell

The PixelForge Design Systems team introduced a prop review step in their component RFC process after noticing that three separate components had shipped a hideOnMobile boolean prop that did slightly different things in each case. The review step requires that any new prop be justified against existing patterns and checked for consistency across similar components before it ships. Prop APIs are treated as public contracts, not implementation details.

State in Component-Driven Development

The most consequential architectural decision in any component-driven codebase is where state lives. Get it wrong — either by keeping state too local when it needs to be shared, or by lifting it too high when it should stay local — and the component hierarchy becomes a mess of prop drilling, unnecessary re-renders, and components that are impossible to test in isolation.

The rule is simple to state and surprisingly hard to apply consistently: state should live as close to where it's used as possible, and no higher. A tooltip's open/closed state belongs inside the Tooltip component. It has no reason to live in a parent component, a context, or a global store. Moving it out of the component doesn't make it more testable — it makes the component less self-contained and the parent more complicated.

Over-Lifted State (Anti-pattern)

A DropdownMenu 's open/closed state is stored in the page-level Redux store. To open the dropdown, a user click dispatches an action. The reducer updates the store. The page re-renders. Three components that share the store re-render unnecessarily. A dropdown open/close — which is entirely ephemeral UI state — now has a paper trail in the Redux DevTools and causes side effects in components that have nothing to do with the dropdown.

Cost: unnecessary re-renders, test complexity, global state pollution.

Correctly Scoped State (Best Practice)

The DropdownMenu manages its own open/closed state internally using useState. No parent knows or cares whether the dropdown is open. The component can be rendered in a Storybook story and its open/close behaviour tested without any store setup. It can be placed on any page without configuration. When it unmounts, its state disappears naturally.

Cost: none. This is the correct scope for ephemeral UI state.

The PixelForge Frontend team uses a three-tier state model that they enforce in code reviews. Local state — ephemeral UI behaviour (open/closed, hover, focus, animation phase) — lives inside the component using useState. Server state — data fetched from the API — is managed with React Query, which handles caching, background refetching, and loading/error states without polluting any client store. Global client state — authenticated user, active theme, feature flags — lives in Zustand, a minimal store that re-renders only the components that subscribe to the specific slice of state they need. Everything that can be local, is local. Only what genuinely needs to be global reaches the global store.

Prop Drilling — The Signal, Not the Problem

Prop drilling — passing state down through multiple layers of components — is often treated as a problem to be solved with context or a global store. But prop drilling is more accurately a signal: if you find yourself passing a prop through four component levels to reach the one that uses it, the component hierarchy might be wrong, not the state location. Before reaching for context, ask whether the intermediate components should be restructured, or whether the consuming component should be repositioned in the tree. Context and global stores are correct tools — but they're used prematurely far more often than they're used late.

How CDD Changes the Delivery Pipeline

The practical impact of component-driven development on delivery speed is not theoretical. Notion's engineering team documented that after adopting a strict CDD discipline with Storybook-first development, their average PR review time for UI changes dropped from 3.1 days to 1.4 days. The reason was mechanical: reviewers could open the Storybook deploy preview, check every component state in isolation, and approve with confidence — instead of needing to reproduce the full application context to evaluate a visual change.

Components developed in isolation also parallelise the work. A team building a new Project Dashboard feature doesn't need the backend API to be ready before work starts. The ProjectCard, ProjectList, and ProjectEmptyState components can be built, reviewed, and merged weeks before the API exists — driven entirely by mock data passed as props. When the API is ready, the data-fetching layer drops into place and the components render real data with zero structural changes.

The PixelForge CDD Delivery Workflow — New Feature

1
Design handoff → Component inventory
The Figma design is reviewed and broken into a component list — atoms, molecules, and organisms required. Existing design system components are identified. Net-new components are listed with their prop interfaces designed before a line of code is written.
2
Build atoms and molecules in Storybook
Engineers build the leaf components first. Each gets stories for every meaningful state. Visual review happens here — no page context required, no running backend. Accessibility testing runs automatically on every story via @storybook/addon-a11y.
3
Compose organisms with mock data
Atoms and molecules are composed into organisms. Each organism story includes a realistic mock data fixture — a fake project object, a fake collaborators array. The organism renders exactly as it will in production, without any live data dependency.
4
Assemble the page template
Organisms are composed into the page template. Layout, responsive breakpoints, and loading skeletons are verified. This is still mock data — the page template can be shared with Product for sign-off before any API integration begins.
5
Wire to real data and routes
The page component wraps the template, connects React Query hooks to real API endpoints, and the mock fixtures are replaced by live data. Component behaviour is unchanged — only the data source changes. Integration testing at the page level confirms end-to-end correctness.

Visual Regression Testing — The CDD Quality Gate

Once components are developed in isolation with Storybook stories covering every state, a powerful quality gate becomes available: visual regression testing. A visual regression test renders a component story, captures a pixel-accurate screenshot, and compares it to a baseline screenshot taken from the last approved version. Any visual difference — a 1px margin change, a colour shift, an unexpected text truncation — is flagged as a diff for human review.

The value is asymmetric. A visual regression test catches a CSS change that accidentally broke the disabled state of a button across every use in the application — something no unit test or integration test would catch, and something that would be invisible in a code review. Chromatic (built specifically for Storybook) and Percy are the two dominant tools in this space. Both integrate with GitHub pull requests to surface visual diffs inline in the review interface.

How Chromatic Works with CDD

On every pull request, Chromatic renders all Storybook stories in a cloud browser environment and diffs them pixel-by-pixel against the last accepted baseline. Changed stories are surfaced in the PR with a side-by-side diff — left is the baseline, right is the new render. The reviewer accepts or rejects each change. Accepted changes become the new baseline. The PixelForge Design Systems team runs Chromatic on every PR touching the component library. A PR that changes the Button component will surface visual diffs for all 14 Button stories simultaneously — not just the one the engineer was explicitly modifying. Unintended side effects are caught before merge, not after production deploy.

The PixelForge Frontend team tracks a metric they call "visual incident rate" — the number of unintended visual regressions that reach production per month. Before Chromatic: 8–12 per month, caught by user reports or QA. After Chromatic on all component library changes: 0–1 per month, with the rare exceptions being page-level layout issues not covered by component-level stories. The incidents that do escape are the ones that motivate adding new stories — every escaped regression becomes a new test case.

The compound return on CDD investment: The discipline pays dividends that multiply over time. A component built correctly in isolation today is a component that can be reused in three future features without rework. A Storybook story written today is a visual regression baseline that protects every future change automatically. A prop API designed carefully today is an API that won't require a migration in six months. The upfront cost of CDD is real — it is slower to build a component in Storybook with all states documented than to wire it directly into a page. But the cumulative return — in reduced bugs, faster reviews, safer refactors, and more confident deployments — compounds with every new component added to the system.

Quiz

1. The PixelForge API for collaborator presence data won't be ready for three weeks, but the Frontend team needs to build the CollaboratorsList feature now. What does a CDD workflow enable them to do?

2. A PixelForge engineer stored a DropdownMenu's open/closed state in the Zustand global store. Three unrelated components now re-render every time any dropdown is toggled. What is the correct fix?

3. A PixelForge engineer updates the Button component's padding to fix a visual issue in the primary variant. The change accidentally shifts the disabled variant's layout by 2px — something invisible in the code diff. What quality gate would catch this before the PR merges?

Up Next
UI Architecture Patterns
PixelForge's Frontend team confronts the moment every growing application reaches — when ad-hoc component structure stops scaling and architectural patterns like container/presenter, compound components, and feature-sliced design become the difference between a codebase that grows cleanly and one that collapses.