
Component Architecture
Component architecture is boundary design: decide what each component owns, what it receives, what it exposes, and what changes should not leak across the tree.
Quick Navigation: Mental Model | Boundaries | Patterns | Organization | State | Interview
Architecture Is Ownership
The core question is not whether a component is small. The core question is whether its responsibility is coherent. A well-designed component has a clear owner, a clear input contract, and a clear reason to re-render.
Good architecture keeps volatile decisions local. Product copy, feature flags, analytics, permissions, server data, and layout constraints should not leak into generic primitives. When those concerns mix, reuse becomes accidental coupling.
Component Boundary Model
Strong components separate visual behavior, feature workflow, route orchestration, and application policy. Each layer can depend inward on simpler layers, but reusable layers should not depend outward on product-specific context.
| Boundary | Owns | Should avoid |
|---|---|---|
| UI primitive | Visual affordance, accessibility behavior, variants, states. | Business rules, data fetching, product-specific branching. |
| Feature component | Feature flow, local interactions, composition of primitives. | Global app policy and unrelated feature state. |
| Route or screen | Data requirements, layout, loading/error states, navigation context. | Low-level button/input behavior and reusable domain primitives. |
| Application shell | Providers, routing, auth/session context, global layout, cross-cutting concerns. | Feature-specific UI decisions and domain workflows. |
Patterns and Trade-offs
Patterns are tools for managing change. The wrong pattern adds indirection; the right one makes ownership obvious.
Container + Presentational
Use when
Data ownership and view rendering need to be separated for testing or reuse.
Trade-off
Can become ceremony if hooks already isolate the side effects cleanly.
Compound Components
Use when
A parent and children need coordinated state with a flexible authoring API.
Trade-off
Great for UI libraries, but harder to type and document than plain props.
Custom Hooks
Use when
Reusable stateful logic should be shared without forcing a visual wrapper.
Trade-off
Hooks share behavior, not UI; they still need stable dependencies and ownership boundaries.
Slots / Children Composition
Use when
The parent owns layout but consumers own specific regions of content.
Trade-off
Keeps APIs flexible, but too many slots can make component behavior hard to predict.
File Organization That Scales
Organize by feature when product domains are the primary source of change. Organize shared primitives by capability when consistency is the primary source of value.
Feature-owned code
features/
checkout/
components/
hooks/
api/
types.ts
index.tsBest when the code changes together and should not become a shared abstraction yet.
Shared platform code
shared/ ui/ hooks/ lib/ telemetry/ design-system/
Best when multiple features rely on the same stable contract.
Component API Design
Component APIs should make valid usage easy and invalid states hard to express. The common staff-level smell is boolean prop growth: each new flag seems harmless, but the combinations become impossible to reason about.
Weak API
<Modal isCheckout isDanger showFooter showCancel primaryLabel="Pay now" />
Many booleans create hidden states. Which combinations are supported?
Stronger API
<Dialog variant="danger"> <Dialog.Header /> <Dialog.Body /> <Dialog.Footer /> </Dialog>
Composition exposes structure while keeping behavior owned by the component.
State Placement
State placement is architecture. Lift state only when shared coordination requires it. Moving state upward too early increases render scope, coupling, and accidental global behavior.
| Ask | Decision |
|---|---|
| Who needs the state? | Keep it local when only one component or small subtree uses it. |
| Is it derived? | Compute it from existing data instead of storing a second source of truth. |
| Does it come from the server? | Treat it as server state with caching, invalidation, loading, and error semantics. |
| Does it coordinate a workflow? | Own it at the feature or route boundary so related steps change together. |
Architecture Smells
- A reusable component imports product-specific stores, routes, or analytics events.
- A component has many boolean props that encode mutually exclusive layouts.
- Server data is copied into local component state without a clear reason.
- A shared component knows about one feature's permissions, copy, or workflow.
- State is lifted globally only because passing props felt inconvenient.
- Every feature creates its own primitives instead of using the shared design system.
Interview Framing
A strong answer starts from change pressure: what changes often, what must stay stable, and which boundary prevents those changes from spreading.
Senior answer pattern
I would separate reusable UI primitives from feature-owned components, keep server data orchestration near route or feature boundaries, expose narrow component contracts, and introduce shared abstractions only after repeated usage proves the contract is stable. Reuse is useful only when it reduces change cost; premature reuse creates coupling.