component placement
why
placement is the cheapest ownership signal: the folder answers whether a component is a library primitive, a route's private detail, or a shared domain concept. three distinct places prevent primitives absorbing business logic and shared components being trapped in the route that created them.
rule
components/ui/ holds shadcn/ui components and directly library-derived primitives only. every authored product component lives outside it, under its actual domain or rendering responsibility.
next.js route boundary
app/owns route segments, framework-required files, visible route composition — not a general component library.- route-private component:
app/<route>/_components/. - used by more than one route: move to root
components/<domain>/.
app/process/page.tsx_components/ProcessSummary.tsxcomponents/process/ProcessSummary.tsx
the two locations are mutually exclusive for an authored component. no app/components/; no shared component left in one route because that route created it first.
placement
- shadcn-derived files stay in
components/ui/with ecosystem kebab-case names (alert-dialog.tsx,dropdown-menu.tsx). - authored shared components:
components/<domain>/, named for domain + rendering responsibility —components/workflow/RunPanel.tsx,components/account/ProfileCard.tsx. - one-route component stays in that route's
_components/until another route proves it shared. - page composition stays in
page.tsx; no one-use page wrapper extraction.
move decision
| observed use | placement |
|---|---|
| one route renders it and no other route imports it | that route's _components/ folder. |
| two or more routes import the same authored component | root components/<domain>/. |
| copied markup looks similar but behavior/vocabulary differs | keep each route-private until a real shared responsibility is proven. |
| library-derived primitive | components/ui/ only. |
| framework route/layout/error/loading file | required app/ location, not components/. |
moving a component: update every import in one forward-only change, delete the route-private copy, no re-export shim at the old path.
this table is the component-scoped instance of the consumer-derived placement ladder in layered source organization; the same ladder governs non-component code.
do not
- add a custom product component to
components/ui/because it looks primitive. - rewrite a library-derived component into a mixed product/business component.
- create a generic component folder when a domain folder owns the behavior and vocabulary.
- put a reusable authored component anywhere under
app/; noapp/components/convenience folder.
verification
- search imports before and after a move.
- route-private component: callers only under its route.
- shared component: no
app/implementation copy, imported throughcomponents/. - list
app/: route segments, framework files, private_components/folders only.
good and bad
bad
app/process/page.tsx // imports ProcessSummaryoverview/page.tsx // also imports ProcessSummarycomponents/ProcessSummary.tsx // ...or components/ui/ProcessSummary.tsx
a shared component sits in the wrong directory (app/components or ui/). when another route needs ProcessSummary, the developer finds it at components/ui/ProcessSummary.tsx, assumes it's a primitive, and customizes it — process-specific styling, rendering logic, state. six months later ui/ProcessSummary mixes button styling (shadcn) with workflow logic (process domain). a button-only primitive update can't touch ProcessSummary without breaking workflow displays: refactoring unsafe, ui primitives polluted with business logic.
good
app/process/page.tsx // imports components/process/ProcessSummaryoverview/page.tsx // also imports components/process/ProcessSummarycomponents/process/ProcessSummary.tsx // shared by multiple process routes
placement answers one question — library-derived, route-private, or shared by a real domain — and the folder states it. ProcessSummary in components/process/ tells every developer it owns workflow display logic, not a primitive. primitives stay in components/ui/, business and design concerns stay separate, refactoring is safe.