Frontend
medium
mid
How would you organize components for maintainability
Organize by feature, not by file type. Separate presentational from container/logic concerns, extract reusable primitives into a shared UI layer, keep components small and single-responsibility, and colocate related files (component, styles, test, hooks).
6 min read·~10 min to think through
Maintainable component organization is about predictable structure, clear boundaries, and low coupling — so a new engineer can find and change things safely.
Folder structure: feature-first, not type-first
Avoid components/, hooks/, utils/ mega-folders that grow forever. Group by feature/domain:
ts
src/
features/
checkout/
components/
hooks/
api/
checkout-page.tsx
catalog/
...
shared/
ui/ # design-system primitives: Button, Modal, Input
hooks/ # truly cross-cutting hooks
lib/ # utils
app/ # routing, providers, layoutEverything a feature needs lives together; deleting a feature is deleting a folder.
Component layering
- UI primitives (
shared/ui) —Button,Input,Modal. No business logic, fully reusable, well-typed props. - Presentational components — receive data via props, render UI, raise events. Easy to test and Storybook.
- Container / feature components — fetch data, hold state, wire things up, compose presentational components.
- Pages / routes — layout and composition only.
Principles
- Single responsibility — a component does one thing. If you can't name it cleanly, it's doing too much.
- Colocation — keep
component.tsx,component.test.tsx,component.module.css, and its local hook in the same folder. - Extract logic into hooks —
useCheckoutForm,usePagination. Keeps components about rendering. - Composition over props explosion — pass
children/slots instead of 15 boolean props. Compound components for related parts (<Tabs><Tabs.Tab/></Tabs>). - Stable public API — features expose a small
index.ts; internals stay private. Don't import deep paths across features. - Keep them small — a 400-line component is a refactor signal.
Smells that mean reorganize
- Props drilled through components that don't use them.
- A "utils" or "components" folder nobody can navigate.
- Circular imports between features.
- The same UI rebuilt three times because no shared primitive exists.
Follow-up questions
- •Why feature-first over type-first folder structure?
- •How do you decide when to extract a hook vs keep logic inline?
- •What's the difference between presentational and container components today, with hooks?
- •How do you enforce that features don't import each other's internals?
Common mistakes
- •Type-first folders (components/, hooks/, utils/) that become unnavigable at scale.
- •Giant multi-hundred-line components doing fetching, state, and rendering.
- •Prop drilling instead of composition or context.
- •No shared UI layer, so the same button is reimplemented everywhere.
Performance considerations
- •Good boundaries enable code-splitting per feature/route. Small focused components make memoization targeted and effective. Colocated, well-bounded code reduces accidental re-renders from oversized shared components.
Edge cases
- •Code shared by exactly two features — promote to shared or keep duplicated?
- •Cross-feature navigation and shared layout.
- •Monorepo vs single app boundaries.
- •Gradual migration of a legacy type-first structure.
Real-world examples
- •A design system package (shared/ui) consumed by every feature, versioned and Storybooked.
- •Feature folders that map 1:1 to routes, each lazy-loaded.
Senior engineer discussion
Seniors frame this around change cost and team scaling: feature-first structure so ownership and deletion are clean, explicit public APIs (index.ts) and lint rules (eslint boundaries) to prevent cross-feature coupling, composition to avoid prop explosions. They treat the presentational/container split as a logic-vs-rendering separation enforced by custom hooks, not dogma.
Related questions
Frontend
Medium
7 min
Frontend
Easy
6 min