As a component gains styles, tests, and stories, how its related files are organized starts to matter. This lesson covers colocating a component's files in its own folder, the presentational-vs-container mental model, and a practical test for knowing when a component has grown too large.
1A Component Is More Than One File
As a component gains styles, tests, and a Storybook story alongside its implementation, cramming everything into one file ā or scattering related files across type-based folders ā stops scaling. Component architecture addresses how a single component's related files should be organized together.
2Colocation: One Folder Per Component
The recommended pattern gives each non-trivial component its own folder holding everything specific to it ā implementation, styles, tests, stories ā with an index.ts re-exporting the component so it can still be imported cleanly from the folder's path.
3Presentational vs. Container Components
A useful mental model splits components into 'presentational' ones that only render UI from received props, with no data-fetching or business logic, and 'container' ones that handle data and pass it down. This isn't a rule to enforce everywhere, but a genuinely useful lens for deciding where logic should live.
4Component Granularity: When to Split
A component that's grown past a couple hundred lines, manages several unrelated pieces of state, or mixes multiple distinct visual sections is usually ready to be split. A practical test: if describing the component's job requires the word 'and', it's likely doing too much and should become several components.
5Step-by-Step Breakdown
A Component Is More Than One File. You've been building components as single .tsx files. As a component gains styles, tests, and a Storybook story, cramming everything into one file ā or scattering it across type-based folders ā stops scaling. Component architecture is about how a single, non-trivial component's related files should actually be organized.
Colocation: One Folder Per Component. The recommended pattern is a folder per non-trivial component, holding everything specific to it ā the component itself, its styles, tests, and stories ā with an index.ts re-exporting the component so it can still be imported cleanly from the folder path.
In the colocation pattern, why does a component's .test.tsx and .module.css file live in the same folder as its .tsx file?
- āEverything specific to one component is kept together, easy to find and delete as a unit
- āIt's required for the build tool to compile the component at all
Presentational vs. Container Components. A useful mental split: 'presentational' components only render UI based on props they receive, with no data-fetching or business logic of their own; 'container' components handle data and pass it down. This isn't a strict rule to enforce everywhere, but it's a genuinely useful lens for deciding where logic belongs.
Component Granularity: When to Split. A component that's grown past ~150-200 lines, handles multiple unrelated pieces of state, or mixes several distinct visual sections is usually a sign it should be split. A useful test: can you describe the component's job in one sentence without using 'and'? If not, it's probably doing too much.
A component's job is described as 'renders the search form AND the results table AND the pagination controls.' What does that suggest?
- āIt's doing too much and should likely be split into separate components
- āThis is a healthy, well-scoped component as-is
Mastery Achieved. You now understand component architecture: colocating a component's implementation, styles, tests, and stories in one folder, the presentational-vs-container mental model for where logic belongs, and using the 'and' test to know when a component has grown too large. Next, you'll learn Atomic Design, a formal system for organizing components by complexity level.
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
This is a code organization convention, not a browser feature.
Fully applicable.
Fully applicable.
Fully applicable.
Accessibility (A11y)
1Colocated Tests Make Accessibility Regression Testing Easier to Maintain
Keeping a component's accessibility-focused tests (like keyboard navigation or ARIA attribute checks) directly alongside its implementation makes it far more likely they get updated whenever the component's markup changes.
SEO Implications
- 1
Component Organization Has No Direct SEO Effect
This is a codebase maintainability concern with no direct bearing on runtime output, server-rendered HTML, or crawlability.
Best Practices
Reserve Dedicated Folders for Genuinely Non-Trivial Components
A tiny, single-file component with no separate styles or tests doesn't need the full colocation-folder treatment ā apply it once a component actually accumulates multiple related files.
Treat the Presentational/Container Split as a Guideline, Not a Rigid Rule
Not every component cleanly fits one category ā small components mixing a bit of local UI state with light data access are fine; use the split as a lens for larger, clearly mixed-responsibility components.
Frequent Bugs
A single component file has grown to several hundred lines and become difficult to navigate or test.
Apply the 'and' test: describe the component's job in one sentence. If it requires 'and', split it into the separate components that sentence implies, each with a single, describable responsibility.
A component's test file is hard to find because it lives in a completely separate top-level tests/ folder, far from the component itself.
Colocate the test file directly alongside the component's implementation (e.g. Button.test.tsx next to Button.tsx) so they're always discovered and updated together.
Real-World Examples
Splitting an Overgrown Dashboard Component
A single Dashboard.tsx file had grown to handle data fetching, a stats summary section, a chart, and a recent-activity feed all in one 400-line file. Applying the 'and' test revealed it was really four responsibilities; splitting it into a DashboardContainer (data fetching) rendering StatsSummary, ActivityChart, and RecentActivityFeed made each piece independently testable and much easier to navigate.
function DashboardContainer() {
const { data } = useDashboardData();
return (
<>
<StatsSummary stats={data.stats} />
<ActivityChart data={data.activity} />
<RecentActivityFeed items={data.recent} />
</>
);
}