
Feature Arch
- 336 installs
- 191 repo stars
- Updated July 24, 2026
- pproenca/dot-skills
feature-arch: A skill for development.
About
feature-arch: A skill for development. This provides functionality for development workflows.
- feature-arch
Feature Arch by the numbers
- 336 all-time installs (skills.sh)
- +9 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,199 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/pproenca/dot-skills --skill feature-archAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 336 |
|---|---|
| repo stars | ★ 191 |
| Last updated | July 24, 2026 |
| Repository | pproenca/dot-skills ↗ |
How do I use feature-arch for development tasks?
Use feature-arch for development tasks
Who is it for?
Best when you're working on backend & apis and need structured help with feature arch.
Skip if: Teams with no backend & apis needs, or anyone wanting a generic chat assistant without this specific workflow.
When should I use this skill?
When you need to use feature-arch for development tasks, or when feature-arch: a skill for development.
What you get
Structured output aligned to feature-arch: feature-arch.
Files
Feature-Based Architecture Best Practices
Comprehensive architecture guide for organizing React applications by features, enabling scalable development with independent teams. Contains 42 rules across 8 categories, prioritized by impact from critical (directory structure, imports) to incremental (naming conventions). When invoked on a real project, the skill produces a project-specific blueprint that anchors every decision in those rules.
Primary Output: The Target Architecture Blueprint
When an agent invokes this skill on a real project, the deliverable is a single markdown file persisted at docs/architecture/FEATURE-ARCH-TARGET.md (or the repo's existing docs location). The blueprint contains:
1. Project context — framework, state libraries, routing model, current shape. 2. Identified features — confirmed with the user, sourced from routes, docs, openspec, and code clusters. 3. Target directory tree — literal paths, not pseudo-trees. 4. Per-feature public APIs — exact named exports of each index.ts. 5. Import-boundary matrix — N×N table of feature relationships (allowed / forbidden / via app / via events). 6. State & data ownership — server-state and client-state owner per feature. 7. Cross-feature communication policy — composition, events, or shared slice — with rationale. 8. Numbered migration plan — file-level move/create/delete steps with S/M/L effort estimates. 9. Human conformance checklist — for code review and "definition of done". 10. Open questions — anything that needs a human decision before migration.
Every section cites the specific rules below that govern its decisions, so the blueprint stays a projection of this skill — not a parallel authority.
How the agent generates the blueprint
Follow the process in references/_blueprint-process.md. Summary:
1. Gather context (package.json, src/ tree, README, CLAUDE.md, openspec/). 2. Identify candidate features from routes, docs, and code clusters. 3. Confirm the feature list with the user via AskUserQuestion. 4. Record explicit decisions (layer model, comm mechanism, state/routing owner). 5. Fill in assets/templates/feature-arch-target.md.template — every {{placeholder}} replaced with a literal project value. 6. Hand off: print path, summarise feature/step counts, list top-3 risks, suggest next action. Do not start executing migration steps in the same turn.
For very small projects (under ~10 source files), produce a one-page seed structure instead and note that a full blueprint should follow after the 2nd–3rd feature exists.
When to Apply
Reference these guidelines when:
- A project asks for a feature-based architecture target (generate the blueprint).
- Creating new features or modules.
- Organizing project directory structure.
- Setting up import rules and boundaries.
- Implementing data fetching patterns.
- Composing components from multiple features.
- Reviewing code for architecture violations.
Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Directory Structure | CRITICAL | struct- |
| 2 | Import & Dependencies | CRITICAL | import- |
| 3 | Module Boundaries | HIGH | bound- |
| 4 | Data Fetching | HIGH | fquery- |
| 5 | Component Organization | MEDIUM-HIGH | fcomp- |
| 6 | State Management | MEDIUM | fstate- |
| 7 | Testing Strategy | MEDIUM | test- |
| 8 | Naming Conventions | LOW | name- |
Quick Reference
1. Directory Structure (CRITICAL)
struct-feature-folders- Organize by feature, not technical typestruct-feature-self-contained- Make features self-containedstruct-shared-layer- Use shared layer for truly generic code onlystruct-flat-hierarchy- Keep directory hierarchy flatstruct-optional-segments- Include only necessary segmentsstruct-app-layer- Separate app layer from features
2. Import & Dependencies (CRITICAL)
import-unidirectional-flow- Enforce unidirectional import flowimport-no-cross-feature- Prohibit cross-feature importsimport-public-api- Export through public API onlyimport-avoid-barrel-files- Avoid deep barrel file re-exportsimport-path-aliases- Use consistent path aliasesimport-type-only- Use type-only imports for types
3. Module Boundaries (HIGH)
bound-feature-isolation- Enforce feature isolationbound-interface-contracts- Define explicit interface contractsbound-feature-scoped-routing- Scope routing to feature concernsbound-minimize-shared-state- Minimize shared state between featuresbound-event-based-communication- Use events for cross-feature communicationbound-feature-size- Keep features appropriately sized
4. Data Fetching (HIGH)
fquery-single-responsibility- Keep query functions single-purposefquery-colocate-with-feature- Colocate data fetching with featuresfquery-parallel-fetching- Fetch independent data in parallelfquery-avoid-n-plus-one- Avoid N+1 query patternsfquery-feature-scoped-keys- Use feature-scoped query keysfquery-server-component-fetching- Fetch at server component level
5. Component Organization (MEDIUM-HIGH)
fcomp-single-responsibility- Apply single responsibility to componentsfcomp-composition-over-props- Prefer composition over prop drillingfcomp-container-presentational- Separate container and presentational concernsfcomp-props-as-data-boundary- Use props as feature boundariesfcomp-colocate-styles- Colocate styles with componentsfcomp-error-boundaries- Use feature-level error boundaries
6. State Management (MEDIUM)
fstate-feature-scoped-stores- Scope state stores to featuresfstate-server-state-separation- Separate server state from client statefstate-lift-minimally- Lift state only as high as necessaryfstate-context-sparingly- Use context sparingly for feature statefstate-reset-on-unmount- Reset feature state on unmount
7. Testing Strategy (MEDIUM)
test-colocate-with-feature- Colocate tests with featurestest-feature-isolation- Test features in isolationtest-shared-utilities- Create feature-specific test utilitiestest-integration-at-app-layer- Write integration tests at app layer
8. Naming Conventions (LOW)
name-feature-naming- Use domain-driven feature namesname-file-conventions- Use consistent file naming conventionsname-descriptive-exports- Use descriptive export names
How to Use
Read individual reference files for detailed explanations and code examples:
- Blueprint process - How to derive a project-specific target architecture
- Blueprint template - Template the agent fills in to produce the end-state document
- Section definitions - Category structure and impact levels
- Rule template - Template for adding new rules
- Individual rules:
references/{prefix}-{slug}.md
Related Skills
- For feature planning, see
feature-specskill - For data fetching, see
tanstack-queryskill - For React component patterns, see
react-19skill
Full Compiled Document
For the complete guide with all rules expanded: AGENTS.md
React Feature-Based Architecture
Version 0.1.0 Community January 2026
Note:
This document is mainly for agents and LLMs to follow when maintaining,
generating, or refactoring codebases. Humans may also find it useful,
but guidance here is optimized for automation and consistency by AI-assisted workflows.
---
Abstract
Comprehensive architecture guide for organizing React applications by features, enabling scalable development with independent teams. Contains 42 rules across 8 categories, prioritized by impact from critical (directory structure and import rules) to incremental (naming conventions). Each rule includes detailed explanations, production-realistic code examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.
---
Table of Contents
1. Directory Structure — CRITICAL
- 1.1 Include Only Necessary Segments — HIGH (Prevents empty folder clutter; keeps features minimal and focused)
- 1.2 Keep Directory Hierarchy Flat — CRITICAL (Reduces cognitive load; prevents 5+ level deep import paths)
- 1.3 Make Features Self-Contained — CRITICAL (Enables independent deployment and parallel team development)
- 1.4 Organize by Feature, Not Technical Type — CRITICAL (Eliminates cross-file navigation; reduces onboarding time by 50%+)
- 1.5 Separate App Layer from Features — HIGH (Isolates global concerns; enables feature modules to remain pure)
- 1.6 Use Shared Layer for Truly Generic Code Only — CRITICAL (Prevents shared/ from becoming a dumping ground; maintains feature boundaries)
2. Import & Dependencies — CRITICAL
- 2.1 Avoid Deep Barrel File Re-exports — HIGH (Prevents tree-shaking failures; reduces bundle size by avoiding unused code)
- 2.2 Enforce Unidirectional Import Flow — CRITICAL (Prevents circular dependencies; enables deterministic build order)
- 2.3 Export Through Public API Only — CRITICAL (Prevents deep imports; enables internal refactoring without breaking consumers)
- 2.4 Prohibit Cross-Feature Imports — CRITICAL (Prevents feature coupling; enables independent feature development)
- 2.5 Use Consistent Path Aliases — HIGH (Eliminates ../../../ chains; makes imports self-documenting)
- 2.6 Use Type-Only Imports for Types — MEDIUM (Enables cross-feature type sharing without runtime coupling)
3. Module Boundaries — HIGH
- 3.1 Define Explicit Interface Contracts — HIGH (Prevents implicit dependencies; enables parallel feature development)
- 3.2 Enforce Feature Isolation — HIGH (Changes in one feature have zero impact on others; enables fearless refactoring)
- 3.3 Keep Features Appropriately Sized — MEDIUM (Right-sized features balance cohesion and manageability)
- 3.4 Minimize Shared State Between Features — HIGH (Reduces coupling surface area; prevents state synchronization bugs)
- 3.5 Scope Routing to Feature Concerns — HIGH (Enables feature-level code splitting; prevents routing configuration sprawl)
- 3.6 Use Events for Cross-Feature Communication — MEDIUM-HIGH (Decouples features at runtime; enables loose coupling without direct imports)
4. Data Fetching — HIGH
- 4.1 Avoid N+1 Query Patterns — HIGH (Prevents request count from scaling with data size; eliminates O(N) network calls)
- 4.2 Colocate Data Fetching with Features — HIGH (Makes features self-contained; enables independent API evolution)
- 4.3 Fetch at Server Component Level — MEDIUM-HIGH (Eliminates client-server waterfalls; reduces bundle size by keeping fetch logic on server)
- 4.4 Fetch Independent Data in Parallel — HIGH (Reduces total load time by ~50% for pages with multiple data sources)
- 4.5 Keep Query Functions Single-Purpose — HIGH (Prevents query permutation explosion as features grow)
- 4.6 Use Feature-Scoped Query Keys — MEDIUM-HIGH (Enables targeted cache invalidation; prevents accidental cache collisions)
5. Component Organization — MEDIUM-HIGH
- 5.1 Apply Single Responsibility to Components — MEDIUM-HIGH (Enables parallel development and isolated testing; reduces component complexity)
- 5.2 Colocate Styles with Components — MEDIUM (Enables complete component portability; prevents orphaned styles)
- 5.3 Prefer Composition Over Prop Drilling — MEDIUM-HIGH (Eliminates prop drilling; enables flexible slot-based component design)
- 5.4 Separate Container and Presentational Concerns — MEDIUM (Enables design system reuse; keeps business logic testable)
- 5.5 Use Feature-Level Error Boundaries — MEDIUM (Isolates failures to single features; prevents full-page crashes)
- 5.6 Use Props as Feature Boundaries — MEDIUM-HIGH (Creates clear interfaces between features; enables feature composition)
6. State Management — MEDIUM
- 6.1 Lift State Only as High as Necessary — MEDIUM (Reduces re-renders; keeps state close to where it's used)
- 6.2 Reset Feature State on Unmount — MEDIUM (Prevents stale state bugs; ensures clean feature initialization)
- 6.3 Scope State Stores to Features — MEDIUM (Prevents global state coupling; enables feature-level state reset and testing)
- 6.4 Separate Server State from Client State — MEDIUM (Eliminates manual cache sync; leverages query library optimizations)
- 6.5 Use Context Sparingly for Feature State — MEDIUM (Prevents context re-render cascades; keeps features portable)
7. Testing Strategy — MEDIUM
- 7.1 Colocate Tests with Features — MEDIUM (Makes test coverage visible; ensures tests move with features)
- 7.2 Create Feature-Specific Test Utilities — MEDIUM (Reduces test boilerplate; ensures consistent test setup)
- 7.3 Test Features in Isolation — MEDIUM (Enables faster tests; provides clear failure attribution)
- 7.4 Write Integration Tests at App Layer — MEDIUM (Verifies feature composition; catches integration bugs)
8. Naming Conventions — LOW
- 8.1 Use Consistent File Naming Conventions — LOW (Enables pattern-based tooling; reduces cognitive load)
- 8.2 Use Descriptive Export Names — LOW (Enables IDE autocomplete; makes imports self-documenting)
- 8.3 Use Domain-Driven Feature Names — LOW (Improves discoverability; aligns code with business terminology)
---
References
1. https://www.robinwieruch.de/react-feature-architecture/ 2. https://feature-sliced.design/ 3. https://github.com/alan2207/bulletproof-react/blob/master/docs/project-structure.md 4. https://legacy.reactjs.org/docs/faq-structure.html
---
Source Files
This document was compiled from individual reference files. For detailed editing or extension:
| File | Description |
|---|---|
| references/_sections.md | Category definitions and impact ordering |
| assets/templates/_template.md | Template for creating new rules |
| SKILL.md | Quick reference entry point |
| metadata.json | Version and reference URLs |
Rule Title Here
1-3 sentences explaining WHY this matters. Focus on maintainability and scalability implications.
Incorrect (what's wrong):
// Bad code example - production-realistic
// Comments explaining the problemCorrect (what's right):
// Good code example - minimal diff from incorrect
// Comments explaining the benefitWhen NOT to use this pattern:
- Exception 1
- Exception 2
Reference: Reference Title
# Feature Architecture Target — {{PROJECT_NAME}}
> Generated by the `feature-arch` skill on {{DATE}}.
> Source commit: `{{GIT_SHA}}`.
> This document is the authoritative target state for the project's
> directory layout, feature boundaries, and import rules. Future changes
> that diverge from this target should either update this document first
> or be flagged as exceptions in PR review.
---
## 1. Project Context
- **Framework:** {{FRAMEWORK}} <!-- e.g., Next.js 16 app router, Vite + React Router 7 -->
- **Server state:** {{SERVER_STATE_LIB}} <!-- e.g., TanStack Query v5, RSC + cache() -->
- **Client state:** {{CLIENT_STATE_LIB}} <!-- e.g., Zustand 5, Context only -->
- **Routing model:** {{ROUTING_MODEL}} <!-- e.g., file-based (Next app), config-based -->
- **Styling:** {{STYLING}}
- **Test runner:** {{TEST_RUNNER}}
- **Current shape:** {{CURRENT_SHAPE}} <!-- one of: already feature-based / technical grouping / page-grouping / greenfield -->
- **Source of feature names:** {{FEATURE_SOURCES}} <!-- e.g., routes under app/, openspec specs, README product sections -->
**Rules applied:** project context anchors every later decision; no rule
is cited here because this section is informational.
---
## 2. Identified Features
| # | Feature | Status | Domain summary | Source signal |
|---|---------|--------|----------------|---------------|
| 1 | `{{feature-1}}` | {{new \| existing \| split-from-x \| merged-from-x+y}} | {{one-sentence domain}} | {{e.g., route /post, README "Posts" section, existing folder src/features/post}} |
| 2 | `{{feature-2}}` | … | … | … |
> Replicate one row per confirmed feature. Drop the row entirely if the
> feature is rejected during user confirmation.
**Rules applied:** [name-feature-naming](../../references/name-feature-naming.md),
[struct-feature-folders](../../references/struct-feature-folders.md),
[bound-feature-size](../../references/bound-feature-size.md).
---
## 3. Target Directory Tree
```
{{PROJECT_ROOT}}/
├── src/
│ ├── app/ # composition layer; no business logic
│ │ ├── pages/ # or `app/` routes for Next.js
│ │ │ └── {{example-page}}.tsx
│ │ ├── providers/ # query client, theme, auth provider
│ │ └── routes.tsx # or framework-equivalent
│ ├── features/
│ │ ├── {{feature-1}}/
│ │ │ ├── api/
│ │ │ │ └── {{verb-noun}}.ts # one file per query/mutation
│ │ │ ├── components/
│ │ │ │ └── {{ComponentName}}.tsx
│ │ │ ├── hooks/
│ │ │ │ └── use{{Thing}}.ts
│ │ │ ├── types/
│ │ │ │ └── {{thing}}.ts
│ │ │ ├── utils/ # internal — never exported
│ │ │ │ └── {{thing-helpers}}.ts
│ │ │ └── index.ts # public API (see §4)
│ │ └── {{feature-2}}/
│ │ └── … # same skeleton
│ └── shared/
│ ├── components/ # UI primitives only (Button, Input)
│ ├── lib/ # generic utils (date, string, fetch wrapper)
│ └── types/ # cross-cutting types only
└── docs/
└── architecture/
└── FEATURE-ARCH-TARGET.md # this document
```
> Replace every `{{…}}` with literal values. Omit any segment a feature
> doesn't need ([struct-optional-segments](../../references/struct-optional-segments.md)).
**Rules applied:** [struct-feature-folders](../../references/struct-feature-folders.md),
[struct-feature-self-contained](../../references/struct-feature-self-contained.md),
[struct-flat-hierarchy](../../references/struct-flat-hierarchy.md),
[struct-app-layer](../../references/struct-app-layer.md),
[struct-shared-layer](../../references/struct-shared-layer.md),
[struct-optional-segments](../../references/struct-optional-segments.md).
---
## 4. Per-Feature Public API
For every feature, list the exact named exports of `src/features/{{feature}}/index.ts`.
Anything not listed here is *internal* and must not be imported by other features
or by the app layer.
### 4.1 `{{feature-1}}`
```typescript
// src/features/{{feature-1}}/index.ts
export { {{ComponentA}}, {{ComponentB}} } from './components';
export { use{{Thing}} } from './hooks/use{{Thing}}';
export type { {{TypeName}} } from './types/{{thing}}';
```
- **Consumers:** {{which other layers/features may import this}} <!-- e.g., app/ only -->
- **Owners of internal-only utilities:** {{utils kept internal}}
### 4.2 `{{feature-2}}`
```typescript
// src/features/{{feature-2}}/index.ts
…
```
**Rules applied:** [import-public-api](../../references/import-public-api.md),
[import-avoid-barrel-files](../../references/import-avoid-barrel-files.md),
[name-descriptive-exports](../../references/name-descriptive-exports.md),
[bound-interface-contracts](../../references/bound-interface-contracts.md).
---
## 5. Import Boundary Matrix
Read row → column. Cell shows what the row feature may do with the column feature.
Allowed values: `allowed`, `forbidden`, `via app` (composition only at app layer),
`via events` (decoupled via event bus, see [bound-event-based-communication](../../references/bound-event-based-communication.md)).
| | shared | app | {{feature-1}} | {{feature-2}} | {{feature-3}} |
|--------------------|--------|-----|---------------|---------------|---------------|
| **shared** | allowed | forbidden | forbidden | forbidden | forbidden |
| **app** | allowed | allowed | allowed | allowed | allowed |
| **{{feature-1}}** | allowed | forbidden | — | {{forbidden\|via app\|via events}} | {{…}} |
| **{{feature-2}}** | allowed | forbidden | {{…}} | — | {{…}} |
| **{{feature-3}}** | allowed | forbidden | {{…}} | {{…}} | — |
> Diagonal is `—`. Row "shared" must be all `forbidden` except itself
> ([import-unidirectional-flow](../../references/import-unidirectional-flow.md)).
> Row "app" can import anything. Feature-to-feature defaults to `via app`
> unless an explicit reason elevates it.
**Rules applied:** [import-unidirectional-flow](../../references/import-unidirectional-flow.md),
[import-no-cross-feature](../../references/import-no-cross-feature.md),
[bound-feature-isolation](../../references/bound-feature-isolation.md),
[bound-event-based-communication](../../references/bound-event-based-communication.md).
---
## 6. State & Data Ownership
| Feature | Server state owned | Client state owned | Shared state consumed |
|---------|--------------------|--------------------|------------------------|
| `{{feature-1}}` | {{e.g., posts query, postById query}} | {{e.g., draftPost zustand store}} | {{e.g., auth user (shared)}} |
| `{{feature-2}}` | … | … | … |
- **Server state library:** {{from §1}}. Query keys are feature-scoped per
[fquery-feature-scoped-keys](../../references/fquery-feature-scoped-keys.md):
`['{{feature}}', '{{entity}}', …]`.
- **Client state library:** {{from §1}}. Each store is colocated under its feature.
**Rules applied:** [fstate-feature-scoped-stores](../../references/fstate-feature-scoped-stores.md),
[fstate-server-state-separation](../../references/fstate-server-state-separation.md),
[fquery-colocate-with-feature](../../references/fquery-colocate-with-feature.md),
[fquery-feature-scoped-keys](../../references/fquery-feature-scoped-keys.md).
---
## 7. Cross-Feature Communication Policy
**Chosen mechanism:** {{composition at app layer \| event bus \| shared store slice}}
**Rationale:** {{1–2 sentences on why this was chosen over the alternatives}}
**Examples in this project:**
- `{{ScenarioA}}` — e.g., "Checkout needs cart items": app layer reads
`useCart()` then passes `items` as a prop to `<Checkout items={…} />`.
- `{{ScenarioB}}` — …
**Rules applied:** [bound-event-based-communication](../../references/bound-event-based-communication.md),
[fcomp-composition-over-props](../../references/fcomp-composition-over-props.md),
[fcomp-props-as-data-boundary](../../references/fcomp-props-as-data-boundary.md).
---
## 8. Migration Plan
Ordered list of file-level changes that move the project from its current
state (§1 "Current shape") to the target (§3). Each step is independently
reviewable; bundle them into PRs at the user's discretion.
| # | Action | Files | Effort (S/M/L) | Blocks |
|---|--------|-------|----------------|--------|
| 1 | Create `src/features/{{feature-1}}/` skeleton (api, components, hooks, index.ts) | new files | S | — |
| 2 | Move `src/components/PostCard.tsx` → `src/features/post/components/PostCard.tsx` | 1 file move | S | 1 |
| 3 | Move `src/hooks/usePost.ts` → `src/features/post/hooks/usePost.ts` | 1 file move | S | 1 |
| 4 | Add `src/features/post/index.ts` with `export { PostCard, usePost }` | new file | S | 2,3 |
| 5 | Update all imports of `@/components/PostCard` and `@/hooks/usePost` to `@/features/post` | grep + replace | M | 4 |
| 6 | Delete now-empty `src/components/`, `src/hooks/` if applicable | delete | S | 5 |
| … | … | … | … | … |
**Total estimated effort:** {{S-count}}S + {{M-count}}M + {{L-count}}L
≈ {{rough person-day estimate}}.
**Rules applied:** all of §3–§7. The migration plan is the execution of
those targets; no new rules are introduced here.
---
## 9. Conformance Checklist (human review)
After all migration steps are complete, verify each item below by walking
the codebase. This is a human checklist; do not gate merges on it
mechanically — use it for code review and onboarding.
### Structure
- [ ] Every business-domain folder lives under `src/features/`.
- [ ] No feature folder is nested more than 2 levels deep below its root.
- [ ] `src/shared/` contains only generic UI primitives and domain-agnostic utils.
- [ ] `src/app/` contains only composition (pages, providers, routes) and no business logic.
- [ ] Every feature has an `index.ts`.
### Imports
- [ ] No file under `src/features/{{feature-A}}/` imports from `src/features/{{feature-B}}/internals` — only from `@/features/{{feature-B}}` (the public API).
- [ ] No file under `src/shared/` imports from `src/features/` or `src/app/`.
- [ ] No file under `src/features/` imports from `src/app/`.
- [ ] Path aliases (e.g., `@/features/*`) are used; relative `../../` chains do not cross feature boundaries.
### Boundaries
- [ ] For each forbidden cell in the matrix (§5), no actual import violates it.
- [ ] Features chosen as `via events` communicate only through the documented event bus, never via direct import.
### Data and state
- [ ] Query keys begin with the feature name (per §6).
- [ ] No global store contains state owned by a single feature.
- [ ] Server state lives in the chosen library; no useState mirroring of fetched data.
### Tests
- [ ] Tests live alongside the feature they test (`*.test.tsx` colocated).
- [ ] Integration tests live in `src/app/` or `tests/integration/`.
### Naming
- [ ] Feature folder names are singular domain nouns in kebab-case.
- [ ] Public exports are descriptive (`PostCard`, not `Card`; `useUser`, not `useData`).
**Rules applied:** spans the entire skill; treat this checklist as the
"definition of done" for the migration.
---
## 10. Open Questions
Items the agent could not resolve and that need a human decision before
the migration proceeds. Each entry must include the options considered.
- {{e.g., "Should `notifications` be a feature or live in `shared/`? Options: feature (clearer ownership), shared (truly cross-cutting). Decision needed by: …"}}
---
## 11. Change Log
| Date | Author | Change |
|------|--------|--------|
| {{DATE}} | {{author/agent}} | Initial blueprint generated. |
{
"version": "1.0.6",
"discipline": "distillation",
"organization": "Community",
"technology": "React Feature-Based Architecture",
"date": "May 2026",
"abstract": "Comprehensive architecture guide for organizing React applications by features, enabling scalable development with independent teams. Contains 42 rules across 8 categories, prioritized by impact from critical (directory structure and import rules) to incremental (naming conventions). When invoked on a project, the skill produces a concrete target-architecture blueprint at docs/architecture/FEATURE-ARCH-TARGET.md — covering project context, identified features, target directory tree, per-feature public APIs, import-boundary matrix, state ownership, cross-feature communication policy, a numbered migration plan, and a human conformance checklist — with every section anchored in the skill's underlying rules.",
"references": [
"https://www.robinwieruch.de/react-feature-architecture/",
"https://feature-sliced.design/",
"https://github.com/alan2207/bulletproof-react/blob/master/docs/project-structure.md",
"https://legacy.reactjs.org/docs/faq-structure.html"
],
"category": "Frontend"
}
Feature-Based Architecture Skill
A comprehensive guide for organizing React applications using feature-based architecture patterns. This skill helps ensure scalable, maintainable codebases by enforcing proper feature isolation, import boundaries, and composition patterns.
When an agent invokes this skill on a real project, it produces a concrete target-architecture blueprint at docs/architecture/FEATURE-ARCH-TARGET.md showing exactly what the project's feature-based structure should look like — directory tree, public APIs, import-boundary matrix, and a numbered migration plan. See `references/_blueprint-process.md` for the discovery and generation workflow, and `assets/templates/feature-arch-target.md.template` for the deliverable shape.
Overview
Feature-based architecture organizes code by business domain rather than technical concerns. Instead of grouping all components in one folder and all hooks in another, code is grouped by the feature it belongs to (user, cart, checkout, etc.).
Key Principles
1. Feature Isolation: Each feature is self-contained and can be developed, tested, and deployed independently 2. Unidirectional Imports: shared → features → app - no backwards imports 3. No Cross-Feature Imports: Features compose at the app layer, not by importing from each other 4. Colocated Code: Tests, styles, and utilities live with the feature they belong to
Structure
.claude/skills/feature-based-architecture/
├── SKILL.md # Entry point with quick reference
├── AGENTS.md # Compiled comprehensive guide (generated)
├── metadata.json # Version and reference information
├── README.md # This file
└── rules/
├── _sections.md # Category definitions
├── _template.md # Rule template
└── *.md # 42 individual rulesCategories
| Category | Prefix | Impact | Rules |
|---|---|---|---|
| Directory Structure | struct- | CRITICAL | 6 |
| Import & Dependencies | import- | CRITICAL | 6 |
| Module Boundaries | bound- | HIGH | 6 |
| Data Fetching | query- | HIGH | 6 |
| Component Organization | comp- | MEDIUM-HIGH | 6 |
| State Management | state- | MEDIUM | 5 |
| Testing Strategy | test- | MEDIUM | 4 |
| Naming Conventions | name- | LOW | 3 |
Usage
This skill is automatically triggered when tasks involve:
- A project asking for a feature-based architecture target (produces the blueprint)
- Project structure decisions
- Feature organization
- Module boundaries
- Cross-feature communication
- Data fetching patterns
- Component composition
Producing the blueprint
When the trigger is "give me the target architecture for this project" (or similar), the agent:
1. Reads the project context (package.json, src/ tree, README, CLAUDE.md, openspec/). 2. Identifies candidate features from routes, docs, and code clusters. 3. Confirms the feature list with the user. 4. Records explicit decisions (layering, cross-feature comm, state and routing owners). 5. Fills in the blueprint template and persists it at docs/architecture/FEATURE-ARCH-TARGET.md. 6. Hands off — does not start executing migration steps in the same turn.
Full workflow lives in `references/_blueprint-process.md`.
References
Blueprint Process: Deriving a Project-Specific Target Architecture
This file is the agent's playbook when this skill is invoked on a real project. The output is a concrete, persisted blueprint at docs/architecture/FEATURE-ARCH-TARGET.md that future agent sessions can read and align changes against.
The rules elsewhere in this skill (struct-*, import-*, bound-*, …) are the principles. This document is the process that turns those principles into a project-shaped end-state. Every decision in the blueprint must cite one or more rules so reviewers can trace the why.
The blueprint is not aspirational fiction. Every feature, path, and public-API entry in it must be either (a) already present in the repo, or (b) listed in the migration plan with a concrete file move / file create step.
---
Step 1: Gather project context (no decisions yet)
The agent reads, never writes, in this step. Collect signals from these sources and keep raw findings — they will be cited in the blueprint's Context section.
1a. Framework and stack
Read package.json (or equivalent). Capture:
- Framework: Next.js (app router vs pages), Vite + React Router, Remix, CRA, Expo, etc.
- Server state: TanStack Query, SWR, RSC +
cache(), Apollo, none. - Client state: Redux Toolkit, Zustand, Jotai, Context-only, Valtio, none.
- Routing model: file-based (Next app dir, Remix, TanStack Router), config-based (React Router declarative), or mixed.
- Styling: Tailwind, CSS Modules, vanilla-extract, styled-components, etc.
- Test runner: Vitest, Jest, Playwright, Cypress.
Framework dictates several blueprint defaults — e.g., Next.js app router moves the "app layer" into app/ routes, and server components shift data-fetching ownership. Record framework explicitly; do not assume.
1b. Current directory shape
Run a depth-3 listing of src/ (or the project root if there is no src/). Classify the current shape as one of:
| Shape | Signals | Implication |
|---|---|---|
| Already feature-based | src/features/*/ or src/modules/*/ or src/domains/*/ exists with >1 child | Blueprint should codify and tighten existing structure, not rewrite it |
| Technical grouping | src/components/, src/hooks/, src/api/, src/utils/ at top level | Blueprint is a migration target; expect a substantial migration plan |
| Page/route grouping | `src/pages/*/components | hooks or app/*/components |
| Greenfield | Few files, mostly scaffolding | Blueprint is the seed structure for new work |
The shape determines the size of the migration plan section, not the shape of the target. The target is always feature-based per the rules in this skill.
1c. Domain language from docs
Read in order, take the first 2–3 that exist:
1. CLAUDE.md, AGENTS.md, .cursor/rules/, or equivalent agent-instruction files 2. README.md (look for product description, "what is X" sections) 3. openspec/ directory — every openspec/specs/* directory name is a candidate capability 4. docs/ — top-level docs imply top-level domains
Extract proper-noun domain terms ("Checkout", "Inventory", "Onboarding", "Workspaces"). These become the feature shortlist seed.
1d. Routes and entry points
If the framework is route-driven, list the route segments. Each top-level route segment is a feature candidate. Examples:
- Next.js app router: each first-level directory under
app/that isn't a
route group (…) or special file.
- React Router: each top-level
<Route>path's first segment. - Remix: each first-level directory under
app/routes/.
1e. Implicit features in existing code
Even with technical grouping, features exist implicitly. Look for:
- Filename clusters:
PostCard.tsx,PostList.tsx,usePost.ts,postApi.ts→ featurepost - Model files: types/models with names that recur across
components/,hooks/,api/ - Route handlers: API routes grouped by resource (
/api/posts,/api/comments)
---
Step 2: Propose a feature list, then confirm with the user
Combine 1c, 1d, and 1e into a candidate feature list. For each candidate, record:
- Name (singular, domain noun, kebab-case folder)
- Source signal (where you saw it: route, docs, filename cluster)
- Confidence (high / medium / low)
Present the list via AskUserQuestion (multi-select) so the user can keep, drop, rename, or split features. Sample question:
"I see these candidate features:post,comment,user,checkout,cart,
notifications. Confirm which belong in your target architecture, or noteany to rename/split."
If the project already has src/features/ (case 1b "Already feature-based"), treat existing folder names as ground truth and only ask about renames, merges, or new features.
After confirmation, fix the feature list. No further additions go in this session's blueprint — additional features get added later via spec.
---
Step 3: Record architectural decisions explicitly
These are the project-specific decisions that the rules in this skill don't make for you. Record each decision and the alternative considered in the blueprint's "Decisions" section.
| Decision | Options | How to choose |
|---|---|---|
| Layer model | features/ + shared/ + app/ (default) <br/> features/ + shared/ + entities/ + app/ (FSD) <br/> route-as-app (Next app router) | Default unless the team uses FSD explicitly or the framework is Next app router |
| Cross-feature communication | Composition at app layer only (default) <br/> Event bus (bound-event-based-communication) <br/> Shared store slice | Composition unless features need to react to each other without app-layer involvement |
| Server state owner | Server components (RSC) <br/> TanStack Query <br/> SWR <br/> Apollo | Match the stack from 1a; do not introduce a new lib |
| Client state owner | Context-only <br/> Zustand <br/> Redux Toolkit | Match the stack from 1a |
| Routing ownership | Feature-scoped (bound-feature-scoped-routing) <br/> App-layer router config | Feature-scoped unless framework imposes app-layer config |
| Public API style | index.ts barrel per feature (import-public-api) <br/> Named-export entrypoint file <br/> Subpath exports (features/x/public) | index.ts per feature unless tree-shaking benchmarks force otherwise |
| Shared layer scope | UI primitives + truly generic utils only (struct-shared-layer) <br/> Includes domain-agnostic hooks <br/> Includes design system | UI primitives + utils unless a design system package already exists |
For each decision, the blueprint records: choice, rationale, rule(s) cited.
---
Step 4: Generate the blueprint
Fill in assets/templates/feature-arch-target.md.template with everything collected so far. The template has placeholder sections — every placeholder must be replaced with concrete project values. Do not ship a blueprint with literal {feature-name} placeholders.
The blueprint goes to docs/architecture/FEATURE-ARCH-TARGET.md unless the project already uses a different docs path; if so, mirror the existing convention (e.g., docs/adr/, documentation/architecture/).
Required content discipline:
- Tree section: literal path strings (
src/features/checkout/api/submit-payment.ts),
not pseudo-trees. Show every directory the agent intends to create or keep.
- Public API section: per feature, list named exports of
index.ts—
export { ComponentName }, export type { TypeName }, export { useHookName }.
- Import boundary matrix: an N×N table where N is the feature count.
Each cell is one of: allowed, forbidden, via app, via events.
- State ownership: per feature, list what server state and what client
state it owns. Cross-cutting state lives in shared/ and is listed once.
- Migration plan: numbered steps. Each step is a single concrete action:
move, create, split, rename, delete, add public export. Each step lists the files involved. Estimate effort (S/M/L) per step.
- Conformance checklist: human-checkable items, no tooling commands —
e.g., [ ] No file under src/features/* imports from another features/* subtree.
---
Step 5: Anchor every claim in a rule
Every section of the blueprint must end with a **Rules applied:** line that links to the rule files in this skill that govern that section. This makes the blueprint a projection of the rules onto the project, not a parallel authority. Example:
**Rules applied:** [struct-feature-folders](../../skills/.curated/feature-arch/references/struct-feature-folders.md),
[struct-feature-self-contained](../../skills/.curated/feature-arch/references/struct-feature-self-contained.md)If a section cannot cite a rule, either (a) it shouldn't be in the blueprint, or (b) a new rule is missing from the skill and should be raised as a follow-up.
---
Step 6: Hand off
The final agent message after generating the blueprint should:
1. Print the blueprint path. 2. Summarise: feature count, migration step count, estimated total effort. 3. List the top 3 highest-risk decisions for the user to review. 4. Suggest the immediate next action — usually "review the blueprint and run /openspec:proposal for the first migration step" or equivalent.
Do not start applying migration steps in the same turn. The blueprint is the deliverable; execution is a separate, user-approved follow-up.
---
When the project is too small for a blueprint
If the project has fewer than ~10 source files, or no clear domain language yet, skip the blueprint and instead output a one-page "seed structure" — a literal target tree for the first feature plus the app/ and shared/ skeletons. Note in the output that this is a seed and a full blueprint should be generated after the second or third feature exists.
Sections
This file defines all sections, their ordering, impact levels, and descriptions. The section ID (in parentheses) is the filename prefix used to group rules.
---
1. Directory Structure (struct)
Impact: CRITICAL Description: Foundation decisions that cascade through all development; wrong structure requires costly rewrites as application scales.
2. Import & Dependencies (import)
Impact: CRITICAL Description: Enforces unidirectional data flow and prevents circular dependencies that cause build failures and runtime bugs.
3. Module Boundaries (bound)
Impact: HIGH Description: Maintains feature isolation preventing changes in one area from causing regressions across the codebase.
4. Data Fetching (fquery)
Impact: HIGH Description: Keeps data logic domain-focused and prevents N+1 query patterns that multiply as features grow.
5. Component Organization (fcomp)
Impact: MEDIUM-HIGH Description: Single-responsibility components enable parallel development and isolated testing.
6. State Management (fstate)
Impact: MEDIUM Description: Feature-scoped state prevents global coupling and enables features to be developed independently.
7. Testing Strategy (test)
Impact: MEDIUM Description: Feature isolation enables faster test execution and clearer failure attribution.
8. Naming Conventions (name)
Impact: LOW Description: Consistent naming aids navigation and onboarding but has no runtime impact.
Use Events for Cross-Feature Communication
When features must communicate without direct dependencies, use an event-based approach. This keeps features loosely coupled while allowing them to react to each other's actions.
Incorrect (direct coupling):
// src/features/order/hooks/useOrder.ts
import { clearCart } from '@/features/cart/stores/cartStore';
import { showNotification } from '@/features/notification/stores/notificationStore';
import { sendAnalytics } from '@/features/analytics/utils/analytics';
export function useOrder() {
async function submitOrder(data: OrderData) {
const order = await createOrder(data);
// Directly calling into other features
clearCart();
showNotification({ type: 'success', message: 'Order placed!' });
sendAnalytics('order_completed', { orderId: order.id });
}
}Correct (event-based communication):
// src/shared/events/eventBus.ts
type EventMap = {
'order:completed': { orderId: string; total: number };
'order:failed': { error: string };
'user:logged-in': { userId: string };
'user:logged-out': void;
};
export const eventBus = createEventBus<EventMap>();
// src/features/order/hooks/useOrder.ts
import { eventBus } from '@/shared/events/eventBus';
export function useOrder() {
async function submitOrder(data: OrderData) {
const order = await createOrder(data);
eventBus.emit('order:completed', { orderId: order.id, total: order.total });
}
}
// src/features/cart/hooks/useCartSync.ts
import { eventBus } from '@/shared/events/eventBus';
import { useCartStore } from '../stores/cartStore';
export function useCartSync() {
useEffect(() => {
return eventBus.on('order:completed', () => {
useCartStore.getState().clearCart();
});
}, []);
}
// src/features/notification/hooks/useOrderNotifications.ts
import { eventBus } from '@/shared/events/eventBus';
export function useOrderNotifications() {
useEffect(() => {
return eventBus.on('order:completed', ({ orderId }) => {
showNotification({ type: 'success', message: `Order ${orderId} placed!` });
});
}, []);
}Benefits:
- Order feature doesn't know about cart, notifications, or analytics
- New features can subscribe to events without modifying order
- Easy to test each feature in isolation
Reference: Feature-Sliced Design
Enforce Feature Isolation
Each feature should be modifiable, testable, and deployable without affecting other features. When features are isolated, refactoring is safe and localized. When they're coupled, every change risks cascading failures.
Incorrect (coupled features):
// src/features/order/hooks/useOrder.ts
import { useCart } from '@/features/cart/hooks/useCart';
import { useUser } from '@/features/user/hooks/useUser';
import { usePayment } from '@/features/payment/hooks/usePayment';
export function useOrder() {
const cart = useCart();
const user = useUser();
const payment = usePayment();
// Tightly coupled to 3 other features
// Change in any feature can break orders
async function submitOrder() {
const order = {
items: cart.items,
userId: user.id,
paymentMethod: payment.selectedMethod,
};
// ...
}
}Correct (isolated with dependency injection):
// src/features/order/hooks/useOrder.ts
interface OrderDependencies {
items: CartItem[];
userId: string;
paymentMethod: PaymentMethod;
}
export function useOrder() {
async function submitOrder(deps: OrderDependencies) {
const order = {
items: deps.items,
userId: deps.userId,
paymentMethod: deps.paymentMethod,
};
// ...
}
return { submitOrder };
}
// src/app/pages/CheckoutPage.tsx - composition at app layer
import { useCart } from '@/features/cart';
import { useUser } from '@/features/user';
import { usePayment } from '@/features/payment';
import { useOrder } from '@/features/order';
export function CheckoutPage() {
const cart = useCart();
const user = useUser();
const payment = usePayment();
const order = useOrder();
const handleSubmit = () => {
order.submitOrder({
items: cart.items,
userId: user.id,
paymentMethod: payment.selectedMethod,
});
};
}Benefits:
- Order feature can be tested with mock dependencies
- Cart, user, and payment can change without breaking orders
- Clear contract between features
Reference: Feature-Sliced Design
Scope Routing to Feature Concerns
Route definitions belong in the app layer, but route parameters and navigation logic relevant to a feature can be encapsulated within that feature. This keeps routing concerns organized while maintaining the app layer's ownership of the route tree.
Incorrect (routing logic scattered):
// src/features/user/components/UserProfile.tsx
import { useNavigate, useParams } from 'react-router-dom';
export function UserProfile() {
const navigate = useNavigate();
const { userId } = useParams(); // Feature assumes route structure
const goToSettings = () => {
navigate(`/users/${userId}/settings`); // Hardcoded route
};
}Correct (feature owns its route utilities):
// src/features/user/routes.ts
export const userRoutes = {
profile: (userId: string) => `/users/${userId}`,
settings: (userId: string) => `/users/${userId}/settings`,
orders: (userId: string) => `/users/${userId}/orders`,
} as const;
// src/features/user/hooks/useUserParams.ts
import { useParams } from 'react-router-dom';
export function useUserParams() {
const { userId } = useParams<{ userId: string }>();
if (!userId) throw new Error('userId is required');
return { userId };
}
// src/features/user/components/UserProfile.tsx
import { useNavigate } from 'react-router-dom';
import { userRoutes } from '../routes';
import { useUserParams } from '../hooks/useUserParams';
export function UserProfile() {
const navigate = useNavigate();
const { userId } = useUserParams();
const goToSettings = () => {
navigate(userRoutes.settings(userId)); // Uses feature's route builder
};
}
// src/app/routes/index.tsx
import { userRoutes } from '@/features/user';
import { UserProfile, UserSettings } from '@/features/user';
export const routes = [
{ path: userRoutes.profile(':userId'), element: <UserProfile /> },
{ path: userRoutes.settings(':userId'), element: <UserSettings /> },
];Benefits:
- Route paths are centralized per feature
- Refactoring routes only requires changes in one place
- Type-safe route parameters
Reference: Feature-Sliced Design
Keep Features Appropriately Sized
Features should be large enough to be meaningful but small enough to be maintainable. A feature that's too small creates unnecessary fragmentation; one that's too large becomes a mini-monolith.
Incorrect (too granular):
src/features/
├── user-avatar/ # Too small - just one component
├── user-name/ # Too small
├── user-email/ # Too small
├── user-profile/ # Could contain all of these
└── user-settings/Incorrect (too large):
src/features/
└── user/
├── components/
│ ├── UserAvatar.tsx
│ ├── UserProfile.tsx
│ ├── UserSettings.tsx
│ ├── UserOrders.tsx # Orders is a separate domain
│ ├── UserPayments.tsx # Payments is a separate domain
│ ├── UserSubscription.tsx # Subscription is a separate domain
│ └── ... 30 more files
└── hooks/
└── ... 20 hooksCorrect (cohesive features):
src/features/
├── user/ # Core user identity
│ ├── components/
│ │ ├── UserAvatar.tsx
│ │ ├── UserProfile.tsx
│ │ └── UserSettings.tsx
│ └── hooks/
│ └── useUser.ts
├── orders/ # Separate domain
│ ├── components/
│ │ ├── OrderList.tsx
│ │ └── OrderDetail.tsx
│ └── hooks/
│ └── useOrders.ts
├── payments/ # Separate domain
│ └── ...
└── subscription/ # Separate domain
└── ...Sizing guidelines:
- 5-15 components per feature is typical
- If a feature has 20+ files, consider splitting
- If a feature has only 1-2 files, consider merging
- Features should map to business domains, not UI components
Signs a feature is too large:
- Multiple developers frequently conflict in the same feature
- Parts of the feature change at different rates
- Some parts are used independently of others
Reference: Robin Wieruch - React Feature Architecture
Define Explicit Interface Contracts
When features need to interact, define explicit interfaces that describe the contract. This makes dependencies visible and allows features to be developed in parallel against the contract.
Incorrect (implicit interface):
// src/features/checkout/components/CheckoutForm.tsx
export function CheckoutForm({ onSuccess }) {
// What shape does onSuccess expect?
// What data should be passed?
const handleSubmit = () => {
onSuccess(someData); // Caller must guess the shape
};
}Correct (explicit contract):
// src/features/checkout/types.ts
export interface CheckoutResult {
orderId: string;
total: number;
items: Array<{ id: string; quantity: number }>;
}
export interface CheckoutFormProps {
userId: string;
cartItems: CartItem[];
onSuccess: (result: CheckoutResult) => void;
onError: (error: CheckoutError) => void;
}
// src/features/checkout/components/CheckoutForm.tsx
export function CheckoutForm({ userId, cartItems, onSuccess, onError }: CheckoutFormProps) {
const handleSubmit = async () => {
try {
const result = await processCheckout(userId, cartItems);
onSuccess({
orderId: result.id,
total: result.total,
items: result.items.map(i => ({ id: i.id, quantity: i.qty })),
});
} catch (err) {
onError(normalizeError(err));
}
};
}Contract patterns:
// Render prop contract
interface UserListProps {
renderUser: (user: User) => ReactNode;
renderEmpty?: () => ReactNode;
}
// Slot contract
interface DashboardProps {
header: ReactNode;
sidebar: ReactNode;
content: ReactNode;
}
// Data contract
interface AnalyticsEvent {
name: string;
properties: Record<string, string | number | boolean>;
timestamp: number;
}Reference: Robin Wieruch - React Feature Architecture
Minimize Shared State Between Features
When multiple features share state, they become implicitly coupled. Changes to that state affect all dependent features. Prefer passing data as props or using feature-local state with explicit synchronization points.
Incorrect (shared global state):
// src/stores/globalStore.ts
export const globalStore = create((set) => ({
user: null,
cart: { items: [] },
notifications: [],
theme: 'light',
// Every feature reaches into this store
}));
// src/features/checkout/components/CheckoutForm.tsx
import { globalStore } from '@/stores/globalStore';
export function CheckoutForm() {
const cart = globalStore(s => s.cart);
const user = globalStore(s => s.user);
// Checkout is now coupled to global store shape
}Correct (feature-scoped state with explicit boundaries):
// src/features/cart/stores/cartStore.ts
export const useCartStore = create((set) => ({
items: [],
addItem: (item) => set(s => ({ items: [...s.items, item] })),
removeItem: (id) => set(s => ({ items: s.items.filter(i => i.id !== id) })),
}));
// src/features/cart/index.ts
export { useCartStore } from './stores/cartStore';
export type { CartItem } from './types';
// src/app/pages/CheckoutPage.tsx
import { useCartStore } from '@/features/cart';
import { CheckoutForm } from '@/features/checkout';
export function CheckoutPage() {
const items = useCartStore(s => s.items);
// App layer reads cart and passes to checkout
return <CheckoutForm items={items} />;
}
// src/features/checkout/components/CheckoutForm.tsx
interface CheckoutFormProps {
items: CartItem[]; // Receives data via props, not global state
}
export function CheckoutForm({ items }: CheckoutFormProps) {
// No knowledge of cart store
}When shared state is acceptable:
- Auth state (current user) - rarely changes, many features need it
- Theme/locale - application-wide concerns
- Feature flags - read-only, system-level
Reference: Feature-Sliced Design
Colocate Styles with Components
Keep component styles in the same location as the component. When styles are centralized or in a global stylesheet, components lose independence and style changes become risky.
Incorrect (centralized styles):
src/
├── styles/
│ ├── components/
│ │ ├── UserCard.css
│ │ ├── PostList.css
│ │ └── CommentSection.css
│ └── global.css
└── features/
└── user/
└── components/
└── UserCard.tsx # Imports from ../../styles/components/Correct (colocated styles):
src/features/user/
├── components/
│ ├── UserCard.tsx
│ ├── UserCard.module.css # CSS Modules
│ └── UserAvatar.tsx
└── ...// src/features/user/components/UserCard.tsx
import styles from './UserCard.module.css';
export function UserCard({ user }: { user: User }) {
return (
<div className={styles.card}>
<UserAvatar user={user} className={styles.avatar} />
<h2 className={styles.name}>{user.name}</h2>
</div>
);
}With Tailwind (styles in component):
// src/features/user/components/UserCard.tsx
export function UserCard({ user }: { user: User }) {
return (
<div className="rounded-lg border bg-white p-4 shadow-sm">
<UserAvatar user={user} className="h-12 w-12 rounded-full" />
<h2 className="mt-2 text-lg font-semibold">{user.name}</h2>
</div>
);
}Shared styles belong in shared:
src/shared/
├── styles/
│ ├── reset.css # Global reset
│ └── variables.css # Design tokens
└── components/
├── Button/
│ ├── Button.tsx
│ └── Button.module.css
└── Input/
├── Input.tsx
└── Input.module.cssBenefits:
- Moving a component moves its styles
- Deleting a component deletes its styles
- No orphaned CSS
Reference: CSS Modules Documentation
Prefer Composition Over Prop Drilling
When components need to render content from different features, use composition (children, render props, slots) instead of passing data down through multiple layers. This keeps components decoupled and flexible.
Incorrect (prop drilling):
// Props must pass through every layer
function Page({ user, cart, notifications }) {
return <Layout user={user} cart={cart} notifications={notifications} />;
}
function Layout({ user, cart, notifications }) {
return (
<div>
<Header user={user} cart={cart} notifications={notifications} />
<Content />
</div>
);
}
function Header({ user, cart, notifications }) {
return (
<header>
<UserMenu user={user} />
<CartIcon cart={cart} />
<NotificationBell notifications={notifications} />
</header>
);
}Correct (composition with slots):
// Layout accepts composed children
interface LayoutProps {
header: ReactNode;
children: ReactNode;
}
function Layout({ header, children }: LayoutProps) {
return (
<div>
<header>{header}</header>
<main>{children}</main>
</div>
);
}
// Page composes features at the top level
function Page() {
return (
<Layout
header={
<>
<UserMenu /> {/* Feature handles its own data */}
<CartIcon /> {/* Feature handles its own data */}
<NotificationBell /> {/* Feature handles its own data */}
</>
}
>
<MainContent />
</Layout>
);
}Render props for flexible rendering:
interface DataTableProps<T> {
data: T[];
renderRow: (item: T) => ReactNode;
renderEmpty?: () => ReactNode;
}
function DataTable<T>({ data, renderRow, renderEmpty }: DataTableProps<T>) {
if (data.length === 0) {
return renderEmpty?.() ?? <EmptyState />;
}
return <table><tbody>{data.map(renderRow)}</tbody></table>;
}
// Usage - feature controls rendering
<DataTable
data={users}
renderRow={(user) => <UserRow user={user} />}
renderEmpty={() => <NoUsersMessage />}
/>Reference: Robin Wieruch - React Feature Architecture
Separate Container and Presentational Concerns
Distinguish between components that manage data/state (containers) and components that render UI (presentational). Presentational components are reusable and easy to test; containers coordinate business logic.
Incorrect (mixed concerns):
// Component does everything - hard to reuse or test
function UserCard() {
const [user, setUser] = useState<User | null>(null);
const [isEditing, setIsEditing] = useState(false);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
const handleSave = async (data: UserData) => {
await updateUser(userId, data);
setUser({ ...user, ...data });
setIsEditing(false);
};
if (!user) return <Loading />;
return (
<div className="card">
{isEditing ? (
<UserForm user={user} onSave={handleSave} />
) : (
<>
<Avatar src={user.avatar} />
<h2>{user.name}</h2>
<p>{user.email}</p>
<button onClick={() => setIsEditing(true)}>Edit</button>
</>
)}
</div>
);
}Correct (separated concerns):
// Presentational - pure rendering, easy to test and reuse
interface UserCardProps {
user: User;
onEdit: () => void;
}
function UserCard({ user, onEdit }: UserCardProps) {
return (
<div className="card">
<Avatar src={user.avatar} />
<h2>{user.name}</h2>
<p>{user.email}</p>
<button onClick={onEdit}>Edit</button>
</div>
);
}
// Container - manages data and state
function UserCardContainer({ userId }: { userId: string }) {
const { data: user, isLoading } = useUser(userId);
const [isEditing, setIsEditing] = useState(false);
const updateMutation = useUpdateUser();
if (isLoading) return <UserCardSkeleton />;
if (isEditing) {
return (
<UserForm
user={user}
onSave={(data) => {
updateMutation.mutate({ userId, data });
setIsEditing(false);
}}
onCancel={() => setIsEditing(false)}
/>
);
}
return <UserCard user={user} onEdit={() => setIsEditing(true)} />;
}Benefits:
- UserCard can be used in Storybook, tests, anywhere
- Business logic is concentrated in container
- Presentational components are pure functions of props
Reference: React Patterns - Container/Presentational
Use Feature-Level Error Boundaries
Wrap each feature's root component in an error boundary. When a feature fails, only that feature shows an error state while the rest of the page remains functional.
Incorrect (single app-level boundary):
// Single error boundary - any feature crash takes down entire app
function App() {
return (
<ErrorBoundary fallback={<FullPageError />}>
<Dashboard />
</ErrorBoundary>
);
}
function Dashboard() {
return (
<div>
<UserProfile /> {/* Crash here = full page error */}
<RecentOrders />
<Notifications />
</div>
);
}Correct (feature-level boundaries):
// src/shared/components/FeatureErrorBoundary.tsx
interface FeatureErrorBoundaryProps {
feature: string;
children: ReactNode;
fallback?: ReactNode;
}
export function FeatureErrorBoundary({
feature,
children,
fallback,
}: FeatureErrorBoundaryProps) {
return (
<ErrorBoundary
fallback={fallback ?? <FeatureErrorFallback feature={feature} />}
onError={(error) => logError(error, { feature })}
>
{children}
</ErrorBoundary>
);
}
// src/app/pages/DashboardPage.tsx
function Dashboard() {
return (
<div>
<FeatureErrorBoundary feature="user-profile">
<UserProfile /> {/* Crash here = only this section shows error */}
</FeatureErrorBoundary>
<FeatureErrorBoundary feature="recent-orders">
<RecentOrders /> {/* Still works even if UserProfile crashed */}
</FeatureErrorBoundary>
<FeatureErrorBoundary feature="notifications">
<Notifications /> {/* Still works */}
</FeatureErrorBoundary>
</div>
);
}Graceful fallback UI:
function FeatureErrorFallback({ feature }: { feature: string }) {
return (
<div className="rounded border border-red-200 bg-red-50 p-4">
<p className="text-red-800">
Unable to load {feature}. <button onClick={retry}>Try again</button>
</p>
</div>
);
}Benefits:
- One feature failing doesn't crash the page
- Errors are attributed to specific features
- Users can continue using working features
Reference: React Error Boundaries
Use Props as Feature Boundaries
When features interact, use props to define the interface. The receiving component should not know about the providing feature's internals. This creates a clear boundary that allows either side to change independently.
Incorrect (feature internals exposed):
// Checkout component knows about Cart's internal structure
import { useCartStore } from '@/features/cart/stores/cartStore';
function CheckoutSummary() {
// Directly accessing cart's internal state structure
const items = useCartStore(s => s.items);
const appliedCoupons = useCartStore(s => s.coupons);
const shippingMethod = useCartStore(s => s.shipping.method);
// If cart store changes structure, this breaks
return <div>...</div>;
}Correct (props as boundary):
// Define explicit interface for what checkout needs
interface CheckoutSummaryProps {
items: Array<{
id: string;
name: string;
price: number;
quantity: number;
}>;
subtotal: number;
discount: number;
shipping: number;
total: number;
}
function CheckoutSummary({ items, subtotal, discount, shipping, total }: CheckoutSummaryProps) {
// Component only knows about its props, not cart internals
return (
<div>
{items.map(item => (
<LineItem key={item.id} item={item} />
))}
<Subtotal amount={subtotal} />
{discount > 0 && <Discount amount={discount} />}
<Shipping amount={shipping} />
<Total amount={total} />
</div>
);
}
// App layer transforms cart state to checkout props
function CheckoutPage() {
const cart = useCartStore();
// Transformation happens at composition point
const summaryProps = {
items: cart.items.map(i => ({
id: i.id,
name: i.product.name,
price: i.product.price,
quantity: i.quantity,
})),
subtotal: cart.getSubtotal(),
discount: cart.getDiscount(),
shipping: cart.getShippingCost(),
total: cart.getTotal(),
};
return <CheckoutSummary {...summaryProps} />;
}Benefits:
- CheckoutSummary doesn't import from cart feature
- Cart can restructure without breaking checkout
- CheckoutSummary is easily testable with mock props
Reference: Robin Wieruch - React Feature Architecture
Apply Single Responsibility to Components
Each component should do one thing well. When a component handles multiple concerns (rendering, data fetching, business logic), it becomes hard to test, reuse, and maintain. Split into focused components.
Incorrect (multiple responsibilities):
// src/features/post/components/Post.tsx
export function Post({ postId }: { postId: string }) {
// Data fetching
const [post, setPost] = useState<Post | null>(null);
const [comments, setComments] = useState<Comment[]>([]);
useEffect(() => {
fetchPost(postId).then(setPost);
fetchComments(postId).then(setComments);
}, [postId]);
// Business logic
const handleLike = async () => { ... };
const handleComment = async () => { ... };
// Rendering post AND comments AND forms
return (
<div>
<h1>{post?.title}</h1>
<p>{post?.content}</p>
<button onClick={handleLike}>Like</button>
<ul>
{comments.map(c => <li key={c.id}>{c.text}</li>)}
</ul>
<CommentForm onSubmit={handleComment} />
</div>
);
}Correct (single responsibility each):
// src/features/post/components/PostContent.tsx
interface PostContentProps {
post: Post;
onLike: () => void;
}
export function PostContent({ post, onLike }: PostContentProps) {
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
<LikeButton count={post.likes} onClick={onLike} />
</article>
);
}
// src/features/comment/components/CommentList.tsx
interface CommentListProps {
comments: Comment[];
}
export function CommentList({ comments }: CommentListProps) {
return (
<ul>
{comments.map(c => <CommentItem key={c.id} comment={c} />)}
</ul>
);
}
// src/app/posts/[id]/page.tsx (composition at app layer)
export async function PostPage({ postId }: { postId: string }) {
const [post, comments] = await Promise.all([
getPost(postId),
getComments(postId),
]);
return (
<>
<PostContent post={post} onLike={() => likePost(postId)} />
<CommentList comments={comments} />
<CommentForm postId={postId} />
</>
);
}Benefits:
- PostContent can be tested without comments
- CommentList can be reused elsewhere
- Each component is ~20-50 lines, easy to understand
Reference: Robin Wieruch - React Feature Architecture
Avoid N+1 Query Patterns
N+1 queries occur when you fetch a list and then individually fetch related data for each item. This creates N+1 requests instead of 2, causing performance to degrade with data growth.
Incorrect (N+1 pattern):
// 1 request for posts + N requests for authors = N+1 total
export async function PostList() {
const posts = await getPosts(); // 1 request
// N additional requests!
const postsWithAuthors = await Promise.all(
posts.map(async (post) => ({
...post,
author: await getUser(post.authorId), // 1 request per post
}))
);
return postsWithAuthors.map(post => <PostCard post={post} />);
}Correct (batched query):
// src/features/user/api/get-users-by-ids.ts
export async function getUsersByIds(ids: string[]) {
return prisma.user.findMany({
where: { id: { in: ids } },
});
}
// 2 requests total regardless of post count
export async function PostList() {
const posts = await getPosts(); // 1 request
const authorIds = [...new Set(posts.map(p => p.authorId))];
const authors = await getUsersByIds(authorIds); // 1 request
const authorsById = new Map(authors.map(a => [a.id, a]));
const postsWithAuthors = posts.map(post => ({
...post,
author: authorsById.get(post.authorId),
}));
return postsWithAuthors.map(post => <PostCard post={post} />);
}Alternative: Lazy load where appropriate:
// If authors are rarely viewed, lazy load on demand
export function PostCard({ post }: { post: Post }) {
const [showAuthor, setShowAuthor] = useState(false);
return (
<article>
<h2>{post.title}</h2>
<button onClick={() => setShowAuthor(true)}>Show Author</button>
{showAuthor && <AuthorInfo userId={post.authorId} />}
</article>
);
}When to accept N+1:
- N is always small (< 5 items)
- Data is heavily cached and cache hits are near 100%
- Lazy loading is appropriate (user rarely views related data)
Reference: Robin Wieruch - React Feature Architecture
Colocate Data Fetching with Features
Data fetching logic belongs within the feature that owns the data. When API calls are scattered in a central api/ folder, features lose independence and changes require coordinating across multiple locations.
Incorrect (centralized API layer):
src/
├── api/
│ ├── users.ts # All user API calls
│ ├── posts.ts # All post API calls
│ ├── comments.ts # All comment API calls
│ └── orders.ts # All order API calls
└── features/
├── user/
│ └── components/ # Components import from ../../../api/users
└── post/
└── components/ # Components import from ../../../api/postsCorrect (colocated with features):
src/features/
├── user/
│ ├── api/
│ │ ├── get-user.ts
│ │ ├── update-user.ts
│ │ └── delete-user.ts
│ ├── components/
│ │ └── UserProfile.tsx
│ └── hooks/
│ └── useUser.ts
└── post/
├── api/
│ ├── get-post.ts
│ ├── get-posts.ts
│ └── create-post.ts
├── components/
│ └── PostList.tsx
└── hooks/
└── usePosts.ts// src/features/user/hooks/useUser.ts
import { getUser } from '../api/get-user';
export function useUser(userId: string) {
return useQuery({
queryKey: ['user', userId],
queryFn: () => getUser(userId),
});
}Benefits:
- Adding a feature includes its API calls
- Removing a feature removes its API calls
- Feature can evolve its API independently
- Related code is always together
Reference: Robin Wieruch - React Feature Architecture
Use Feature-Scoped Query Keys
Query keys should be hierarchical with the feature name as the root. This enables precise cache invalidation and prevents key collisions between features.
Incorrect (flat, collision-prone keys):
// src/features/user/hooks/useUser.ts
useQuery({ queryKey: ['user', userId], ... });
// src/features/admin/hooks/useUser.ts
useQuery({ queryKey: ['user', userId], ... }); // Collides with above!
// Hard to invalidate all user queries
queryClient.invalidateQueries({ queryKey: ['user'] }); // Might affect admin tooCorrect (feature-scoped key factory):
// src/features/user/query-keys.ts
export const userKeys = {
all: ['user'] as const,
lists: () => [...userKeys.all, 'list'] as const,
list: (filters: UserFilters) => [...userKeys.lists(), filters] as const,
details: () => [...userKeys.all, 'detail'] as const,
detail: (id: string) => [...userKeys.details(), id] as const,
};
// src/features/user/hooks/useUser.ts
import { userKeys } from '../query-keys';
export function useUser(userId: string) {
return useQuery({
queryKey: userKeys.detail(userId),
queryFn: () => getUser(userId),
});
}
// src/features/admin/query-keys.ts
export const adminUserKeys = {
all: ['admin', 'user'] as const,
detail: (id: string) => [...adminUserKeys.all, 'detail', id] as const,
};Invalidation patterns:
// Invalidate all user data
queryClient.invalidateQueries({ queryKey: userKeys.all });
// Invalidate only user lists (not details)
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
// Invalidate specific user
queryClient.invalidateQueries({ queryKey: userKeys.detail(userId) });Benefits:
- Clear ownership of cache keys
- Predictable invalidation scope
- No accidental cross-feature cache interference
Reference: TanStack Query - Query Keys
Fetch Independent Data in Parallel
When a component needs multiple pieces of unrelated data, fetch them in parallel using Promise.all(). Sequential fetching creates waterfalls where total time equals the sum of all requests.
Incorrect (sequential waterfall):
// Each request waits for the previous one
// Total time: 200ms + 150ms + 100ms = 450ms
export async function DashboardPage() {
const user = await getUser(userId); // 200ms
const orders = await getOrders(userId); // 150ms
const notifications = await getNotifications(userId); // 100ms
return <Dashboard user={user} orders={orders} notifications={notifications} />;
}Correct (parallel fetching):
// All requests start simultaneously
// Total time: max(200ms, 150ms, 100ms) = 200ms
export async function DashboardPage() {
const [user, orders, notifications] = await Promise.all([
getUser(userId), // 200ms
getOrders(userId), // 150ms
getNotifications(userId), // 100ms
]);
return <Dashboard user={user} orders={orders} notifications={notifications} />;
}With React Query:
// src/app/pages/DashboardPage.tsx
export function DashboardPage({ userId }: { userId: string }) {
// These queries run in parallel automatically
const userQuery = useUser(userId);
const ordersQuery = useOrders(userId);
const notificationsQuery = useNotifications(userId);
if (userQuery.isLoading || ordersQuery.isLoading || notificationsQuery.isLoading) {
return <Loading />;
}
return (
<Dashboard
user={userQuery.data}
orders={ordersQuery.data}
notifications={notificationsQuery.data}
/>
);
}When sequential is necessary:
- Second request depends on first request's result
- Rate limiting requires throttled requests
- User must complete a step before seeing next data
Reference: Robin Wieruch - React Feature Architecture
Fetch at Server Component Level
In React Server Component architectures, fetch data in server components and pass to client components as props. This eliminates client-server waterfalls and keeps data fetching off the client bundle.
Incorrect (client component fetching):
// src/features/post/components/PostPage.tsx
'use client';
export function PostPage({ postId }: { postId: string }) {
const [post, setPost] = useState<Post | null>(null);
useEffect(() => {
fetch(`/api/posts/${postId}`) // Client-server waterfall
.then(res => res.json())
.then(setPost);
}, [postId]);
if (!post) return <Loading />;
return <PostContent post={post} />;
}Correct (server component fetching):
// src/features/post/components/PostPage.tsx (Server Component)
import { getPost } from '../api/get-post';
import { PostContent } from './PostContent'; // Client component
export async function PostPage({ postId }: { postId: string }) {
const post = await getPost(postId); // Fetches on server
return <PostContent post={post} />;
}
// src/features/post/components/PostContent.tsx
'use client';
interface PostContentProps {
post: Post; // Receives data as props, no fetching
}
export function PostContent({ post }: PostContentProps) {
const [likes, setLikes] = useState(post.likes);
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
<LikeButton likes={likes} onLike={() => setLikes(l => l + 1)} />
</article>
);
}Composition pattern:
// src/app/posts/[id]/page.tsx (Server Component)
import { PostPage } from '@/features/post';
import { Comments } from '@/features/comment';
export default async function Page({ params }: { params: { id: string } }) {
// Parallel fetch at app layer
const [post, comments] = await Promise.all([
getPost(params.id),
getComments(params.id),
]);
return (
<>
<PostPage post={post} />
<Comments comments={comments} />
</>
);
}Reference: Next.js - Data Fetching
Keep Query Functions Single-Purpose
Each query function should fetch one type of data. Avoid creating variations that combine multiple concerns. When features need combined data, fetch separately and compose at the component level.
Incorrect (query permutations):
// src/features/post/api/queries.ts
// Creates combinatorial explosion as requirements grow
export async function getPost(id: string) { ... }
export async function getPostWithComments(id: string) { ... }
export async function getPostWithAuthor(id: string) { ... }
export async function getPostWithCommentsAndAuthor(id: string) { ... }
export async function getPostWithCommentsAndAuthorAndLikes(id: string) { ... }
// N relations = 2^N possible combinationsCorrect (single-purpose queries):
// src/features/post/api/get-post.ts
export async function getPost(id: string) {
return prisma.post.findUnique({ where: { id } });
}
// src/features/comment/api/get-comments.ts
export async function getComments(postId: string) {
return prisma.comment.findMany({ where: { postId } });
}
// src/features/user/api/get-user.ts
export async function getUser(id: string) {
return prisma.user.findUnique({ where: { id } });
}
// Component composes what it needs
export async function PostPage({ postId }: { postId: string }) {
const [post, comments] = await Promise.all([
getPost(postId),
getComments(postId),
]);
return (
<article>
<PostContent post={post} />
<CommentList comments={comments} />
</article>
);
}Benefits:
- Linear growth: N relations = N query functions
- Each query is independently cacheable
- Parallel fetching via Promise.all()
- Each feature owns its own data fetching
Reference: Robin Wieruch - React Feature Architecture
Use Context Sparingly for Feature State
Context is useful for dependency injection and app-wide configuration, but causes re-render cascades when used for frequently-changing state. Prefer feature stores or local state for dynamic data.
Incorrect (frequently changing data in context):
// Every context consumer re-renders on any cart change
const CartContext = createContext<CartContextValue | null>(null);
function CartProvider({ children }) {
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0);
const addItem = (item) => {
setItems([...items, item]); // Re-renders all consumers
setTotal(total + item.price);
};
return (
<CartContext.Provider value={{ items, total, addItem }}>
{children} {/* Every consumer re-renders */}
</CartContext.Provider>
);
}
// Components re-render even if they only use `total`
function CartIcon() {
const { total } = useContext(CartContext); // Re-renders when items change
return <span>{total}</span>;
}Correct (store with selectors):
// src/features/cart/stores/cartStore.ts
export const useCartStore = create((set, get) => ({
items: [],
addItem: (item) => set(s => ({ items: [...s.items, item] })),
getTotal: () => get().items.reduce((sum, i) => sum + i.price, 0),
}));
// Only re-renders when selected state changes
function CartIcon() {
const total = useCartStore(s => s.items.reduce((sum, i) => sum + i.price, 0));
return <span>{total}</span>;
}
function CartItemCount() {
const count = useCartStore(s => s.items.length); // Only re-renders when count changes
return <span>{count}</span>;
}When context is appropriate:
- Dependency injection (API client, auth)
- Theme/locale (changes rarely)
- Feature flags (read-only)
When to use stores:
- Frequently updating state
- Multiple components need different slices
- Need fine-grained subscriptions
Reference: Zustand Documentation
Scope State Stores to Features
Each feature should own its state store. When state is global, features become coupled through shared state, making them impossible to develop, test, or remove independently.
Incorrect (global monolithic store):
// src/stores/store.ts
export const useStore = create((set) => ({
// User feature state
user: null,
userLoading: false,
setUser: (user) => set({ user }),
// Cart feature state
cartItems: [],
addToCart: (item) => set(s => ({ cartItems: [...s.cartItems, item] })),
// Notification feature state
notifications: [],
addNotification: (n) => set(s => ({ notifications: [...s.notifications, n] })),
// Everything mixed together - impossible to isolate
}));Correct (feature-scoped stores):
// src/features/user/stores/userStore.ts
export const useUserStore = create((set) => ({
user: null,
isLoading: false,
setUser: (user) => set({ user }),
clearUser: () => set({ user: null }),
}));
// src/features/cart/stores/cartStore.ts
export const useCartStore = create((set) => ({
items: [],
addItem: (item) => set(s => ({ items: [...s.items, item] })),
removeItem: (id) => set(s => ({ items: s.items.filter(i => i.id !== id) })),
clearCart: () => set({ items: [] }),
}));
// src/features/notification/stores/notificationStore.ts
export const useNotificationStore = create((set) => ({
notifications: [],
add: (n) => set(s => ({ notifications: [...s.notifications, n] })),
dismiss: (id) => set(s => ({
notifications: s.notifications.filter(n => n.id !== id),
})),
}));Feature exposes store via public API:
// src/features/cart/index.ts
export { useCartStore } from './stores/cartStore';
export type { CartItem } from './types';
// Other features use the exported store
import { useCartStore } from '@/features/cart';Benefits:
- Feature can be removed along with its store
- Tests can reset feature state independently
- Clear ownership of state
Reference: Feature-Sliced Design
Lift State Only as High as Necessary
State should live in the lowest common ancestor of components that need it. Lifting state too high causes unnecessary re-renders and makes the state's purpose unclear.
Incorrect (state lifted too high):
// State in app root - causes full tree re-render on every keystroke
function App() {
const [searchQuery, setSearchQuery] = useState('');
const [sortOrder, setSortOrder] = useState('asc');
const [selectedTab, setSelectedTab] = useState('all');
return (
<div>
<Header />
<Sidebar selectedTab={selectedTab} onSelectTab={setSelectedTab} />
<ProductList
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
sortOrder={sortOrder}
onSortChange={setSortOrder}
/>
<Footer /> {/* Re-renders on every search keystroke */}
</div>
);
}Correct (state at lowest necessary level):
function App() {
return (
<div>
<Header />
<MainContent />
<Footer /> {/* Never re-renders due to search/sort */}
</div>
);
}
function MainContent() {
const [selectedTab, setSelectedTab] = useState('all');
return (
<>
<Sidebar selectedTab={selectedTab} onSelectTab={setSelectedTab} />
<ProductList selectedTab={selectedTab} />
</>
);
}
function ProductList({ selectedTab }: { selectedTab: string }) {
// Search and sort state only affects ProductList subtree
const [searchQuery, setSearchQuery] = useState('');
const [sortOrder, setSortOrder] = useState('asc');
return (
<div>
<SearchInput value={searchQuery} onChange={setSearchQuery} />
<SortDropdown value={sortOrder} onChange={setSortOrder} />
<ProductGrid tab={selectedTab} search={searchQuery} sort={sortOrder} />
</div>
);
}Decision guide:
| Situation | Where to put state |
|---|---|
| Single component uses it | In that component |
| Sibling components share it | In parent |
| Distant components share it | Context or store |
| Server data | Query library |
Reference: React Docs - Sharing State
Reset Feature State on Unmount
When a feature unmounts, reset its state to prevent stale data from appearing when the feature remounts. Persistent stores can cause bugs when users navigate away and back.
Incorrect (stale state persists):
// User views checkout, abandons, browses, returns to checkout
// Sees old form data from previous session
const useCheckoutStore = create((set) => ({
shippingAddress: null,
paymentMethod: null,
setShipping: (addr) => set({ shippingAddress: addr }),
}));
function CheckoutPage() {
const { shippingAddress } = useCheckoutStore();
// shippingAddress still has old data from previous visit
return <CheckoutForm defaultAddress={shippingAddress} />;
}Correct (reset on unmount):
// src/features/checkout/stores/checkoutStore.ts
const initialState = {
shippingAddress: null,
paymentMethod: null,
step: 1,
};
export const useCheckoutStore = create((set) => ({
...initialState,
setShipping: (addr) => set({ shippingAddress: addr }),
setPayment: (method) => set({ paymentMethod: method }),
nextStep: () => set(s => ({ step: s.step + 1 })),
reset: () => set(initialState),
}));
// src/features/checkout/components/CheckoutPage.tsx
function CheckoutPage() {
const reset = useCheckoutStore(s => s.reset);
useEffect(() => {
// Reset when leaving checkout
return () => reset();
}, [reset]);
return <CheckoutForm />;
}Alternative: Feature-scoped store instance:
// src/features/checkout/CheckoutProvider.tsx
const CheckoutContext = createContext<CheckoutStore | null>(null);
export function CheckoutProvider({ children }) {
// New store instance created each mount
const storeRef = useRef<CheckoutStore>();
if (!storeRef.current) {
storeRef.current = createCheckoutStore();
}
return (
<CheckoutContext.Provider value={storeRef.current}>
{children}
</CheckoutContext.Provider>
);
}
// Store is automatically fresh on each mountWhen NOT to reset:
- User preferences (theme, language)
- Draft content (auto-saved forms)
- Explicitly preserved state (shopping cart)
Reference: Zustand - Resetting State
Separate Server State from Client State
Server state (data from API) and client state (UI state, form state) have different characteristics. Server state should be managed by a query library; client state by local state or stores. Mixing them leads to stale data and sync bugs.
Incorrect (server state in client store):
// src/stores/userStore.ts
export const useUserStore = create((set) => ({
users: [],
isLoading: false,
// Manual fetching logic
fetchUsers: async () => {
set({ isLoading: true });
const users = await api.getUsers();
set({ users, isLoading: false });
},
// Manual cache invalidation
invalidate: () => {
// How do we know when to refetch?
// What about stale data?
// What about deduplication?
},
}));Correct (server state in query library):
// src/features/user/hooks/useUsers.ts
// Server state - managed by TanStack Query
export function useUsers(filters: UserFilters) {
return useQuery({
queryKey: userKeys.list(filters),
queryFn: () => getUsers(filters),
staleTime: 60_000, // Built-in cache management
});
}
// src/features/user/stores/userUIStore.ts
// Client state - UI-only concerns
export const useUserUIStore = create((set) => ({
selectedUserId: null,
filterPanelOpen: false,
sortOrder: 'asc' as const,
selectUser: (id) => set({ selectedUserId: id }),
toggleFilterPanel: () => set(s => ({ filterPanelOpen: !s.filterPanelOpen })),
setSortOrder: (order) => set({ sortOrder: order }),
}));Usage:
function UserListPage() {
// Server state
const { data: users, isLoading } = useUsers({ active: true });
// Client state
const { selectedUserId, selectUser, sortOrder } = useUserUIStore();
const sortedUsers = useMemo(() =>
[...(users ?? [])].sort((a, b) =>
sortOrder === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
),
[users, sortOrder]
);
return <UserList users={sortedUsers} selected={selectedUserId} onSelect={selectUser} />;
}Server state characteristics:
- Fetched from external source
- Can become stale
- Needs refetching, deduplication, caching
Client state characteristics:
- Created locally
- Never stale (source of truth is client)
- No network concerns
Reference: TanStack Query - Overview
Avoid Deep Barrel File Re-exports
While feature index.ts files are useful for public APIs, avoid creating nested barrel files that re-export everything. Deep barrel chains prevent bundlers from tree-shaking unused code and can cause performance issues in development.
Incorrect (barrel chain):
// src/features/user/components/index.ts
export * from './UserProfile';
export * from './UserSettings';
export * from './UserAvatar';
export * from './UserBadge';
// ... 20 more exports
// src/features/user/index.ts
export * from './components'; // Re-exports everything
export * from './hooks';
export * from './utils';
// Consumer imports one component but bundles all
import { UserAvatar } from '@/features/user';Correct (explicit exports):
// src/features/user/index.ts
// Explicit, named exports - bundler knows exactly what's used
export { UserProfile } from './components/UserProfile';
export { UserSettings } from './components/UserSettings';
export { UserAvatar } from './components/UserAvatar';
export { useUser } from './hooks/useUser';
export type { User } from './types';
// Consumer
import { UserAvatar } from '@/features/user'; // Only UserAvatar bundledAlternative for large features:
// Direct imports for specific needs
import { UserAvatar } from '@/features/user/components/UserAvatar';
// This is acceptable when:
// 1. Feature has 15+ exports
// 2. Consumer only needs one specific item
// 3. Bundle size is criticalWhen barrel files are OK:
- Feature public API (index.ts) with explicit exports
- Small features with < 10 exports
- Type-only exports (no runtime impact)
Reference: Bulletproof React - Project Structure
Prohibit Cross-Feature Imports
Features must not import directly from other features. When features need to interact, compose them at the app layer. Direct cross-feature imports create hidden dependencies that make features impossible to modify independently.
Incorrect (cross-feature imports):
// src/features/checkout/components/CheckoutSummary.tsx
import { ProductCard } from '@/features/product/components/ProductCard'; // WRONG
import { useCart } from '@/features/cart/hooks/useCart'; // WRONG
import { UserAddress } from '@/features/user/components/UserAddress'; // WRONG
export function CheckoutSummary() {
const cart = useCart();
return (
<div>
{cart.items.map(item => <ProductCard product={item} />)}
<UserAddress />
</div>
);
}Correct (composition at app layer):
// src/features/checkout/components/CheckoutSummary.tsx
interface CheckoutSummaryProps {
items: CartItem[];
renderProduct: (item: CartItem) => ReactNode;
addressSection: ReactNode;
}
export function CheckoutSummary({ items, renderProduct, addressSection }: CheckoutSummaryProps) {
return (
<div>
{items.map(renderProduct)}
{addressSection}
</div>
);
}
// src/app/pages/CheckoutPage.tsx
import { CheckoutSummary } from '@/features/checkout';
import { ProductCard } from '@/features/product';
import { UserAddress } from '@/features/user';
import { useCart } from '@/features/cart';
export function CheckoutPage() {
const cart = useCart();
return (
<CheckoutSummary
items={cart.items}
renderProduct={(item) => <ProductCard product={item} />}
addressSection={<UserAddress />}
/>
);
}ESLint enforcement per feature:
// .eslintrc.js
rules: {
'import/no-restricted-paths': ['error', {
zones: [
{ target: './src/features/checkout', from: './src/features/product' },
{ target: './src/features/checkout', from: './src/features/cart' },
{ target: './src/features/checkout', from: './src/features/user' },
// Add for each feature combination
],
}],
}Reference: Robin Wieruch - React Feature Architecture
Use Consistent Path Aliases
Configure path aliases to avoid relative import chains. Aliases make imports self-documenting by showing feature ownership clearly and survive file relocations within the same feature.
Incorrect (deep relative paths):
// src/features/checkout/components/PaymentForm.tsx
import { Button } from '../../../shared/components/Button';
import { useAuth } from '../../../features/auth/hooks/useAuth'; // Also wrong: cross-feature
import { formatCurrency } from '../../../shared/utils/formatCurrency';
import { useCheckout } from '../hooks/useCheckout';Correct (path aliases):
// src/features/checkout/components/PaymentForm.tsx
import { Button } from '@/shared/components/Button';
import { formatCurrency } from '@/shared/utils/formatCurrency';
import { useCheckout } from '../hooks/useCheckout'; // Same feature = relative OKtsconfig.json:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@/shared/*": ["src/shared/*"],
"@/features/*": ["src/features/*"],
"@/app/*": ["src/app/*"]
}
}
}Guidelines:
- Use
@/prefix for absolute imports from src - Use relative imports (
./,../) within the same feature - Relative imports within a feature make the feature more portable
Vite configuration:
// vite.config.ts
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});Reference: Robin Wieruch - React Folder Structure
Export Through Public API Only
Each feature should have a single entry point (index.ts) that exports its public API. External code should never import internal files directly. This allows internal restructuring without affecting consumers.
Incorrect (deep imports into feature internals):
// src/app/pages/UserPage.tsx
import { UserProfile } from '@/features/user/components/UserProfile';
import { useUser } from '@/features/user/hooks/useUser';
import { formatUserName } from '@/features/user/utils/formatters';
import { User } from '@/features/user/types/user';Correct (import from public API):
// src/features/user/index.ts (public API)
export { UserProfile } from './components/UserProfile';
export { UserSettings } from './components/UserSettings';
export { useUser } from './hooks/useUser';
export type { User, UserRole } from './types';
// Note: formatUserName is NOT exported - it's internal
// src/app/pages/UserPage.tsx
import { UserProfile, useUser } from '@/features/user';
import type { User } from '@/features/user';Internal file can import freely:
// src/features/user/components/UserProfile.tsx
import { useUser } from '../hooks/useUser';
import { formatUserName } from '../utils/formatters'; // Internal util
import type { User } from '../types';ESLint enforcement:
// .eslintrc.js
rules: {
'no-restricted-imports': ['error', {
patterns: [
{
group: ['@/features/*/components/*', '@/features/*/hooks/*', '@/features/*/utils/*'],
message: 'Import from feature index.ts instead',
},
],
}],
}Benefits:
- Refactor internal structure without breaking external imports
- Clear contract of what a feature provides
- Smaller, focused public surface area
Reference: Feature-Sliced Design
Use Type-Only Imports for Types
Use import type syntax when importing only TypeScript types. This ensures types are stripped at compile time and allows sharing types across features without creating runtime dependencies.
Incorrect (mixing type and value imports):
// src/features/checkout/components/CheckoutForm.tsx
import { User, useUser } from '@/features/user'; // Creates runtime dependency for type
export function CheckoutForm({ userId }: { userId: string }) {
// We only need the User type, not useUser
const [user, setUser] = useState<User | null>(null);
}Correct (separate type imports):
// src/features/checkout/components/CheckoutForm.tsx
import type { User } from '@/features/user'; // Type-only, no runtime dependency
export function CheckoutForm({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
}Shared types for cross-feature contracts:
// src/shared/types/entities.ts
export interface User {
id: string;
email: string;
name: string;
}
export interface Product {
id: string;
name: string;
price: number;
}
// Features import shared types
// src/features/checkout/types.ts
import type { User, Product } from '@/shared/types/entities';
export interface CheckoutItem {
product: Product;
quantity: number;
}
export interface CheckoutSession {
user: User;
items: CheckoutItem[];
}Benefits:
- No runtime bundle impact for type-only imports
- Clear distinction between runtime and compile-time dependencies
- Enables type sharing without architectural coupling
TypeScript configuration:
{
"compilerOptions": {
"verbatimModuleSyntax": true // Enforces type-only imports
}
}Reference: TypeScript Handbook - Type-Only Imports
Enforce Unidirectional Import Flow
Imports must flow in one direction: shared → features → app. Features can import from shared, and app can import from both, but never the reverse. This prevents circular dependencies that cause build failures and makes the dependency graph predictable.
Incorrect (bidirectional imports):
// src/shared/utils/analytics.ts
import { useAuth } from '@/features/auth/hooks/useAuth'; // WRONG: shared → features
export function trackEvent(event: string) {
const { user } = useAuth();
// ...
}// src/features/user/components/UserProfile.tsx
import { AppLayout } from '@/app/layouts/AppLayout'; // WRONG: features → app
export function UserProfile() {
return <AppLayout>...</AppLayout>;
}Correct (unidirectional flow):
// Dependency flow: shared → features → app
// src/shared/utils/analytics.ts
export function trackEvent(event: string, userId?: string) {
// No feature imports - userId passed as parameter
}
// src/features/user/components/UserProfile.tsx
import { formatDate } from '@/shared/utils/formatDate'; // OK: shared used by feature
import { trackEvent } from '@/shared/utils/analytics';
export function UserProfile({ user }) {
useEffect(() => {
trackEvent('profile_view', user.id);
}, []);
return <div>...</div>;
}
// src/app/pages/UserPage.tsx
import { UserProfile } from '@/features/user'; // OK: app uses features
import { AppLayout } from '@/app/layouts/AppLayout';
export function UserPage() {
return (
<AppLayout>
<UserProfile />
</AppLayout>
);
}ESLint enforcement:
// .eslintrc.js
rules: {
'import/no-restricted-paths': ['error', {
zones: [
{ target: './src/shared', from: './src/features' },
{ target: './src/shared', from: './src/app' },
{ target: './src/features', from: './src/app' },
],
}],
}Reference: Bulletproof React - Project Structure
Use Descriptive Export Names
Export names should be descriptive and unique across the codebase. Generic names like Card, List, or Button cause confusion when multiple features export similar components.
Incorrect (generic export names):
// src/features/user/components/Card.tsx
export function Card({ user }) { ... } // Which card?
// src/features/product/components/Card.tsx
export function Card({ product }) { ... } // Collision!
// Import confusion
import { Card } from '@/features/user'; // UserCard? ProductCard?
import { Card as ProductCard } from '@/features/product'; // Requires aliasCorrect (descriptive export names):
// src/features/user/components/UserCard.tsx
export function UserCard({ user }: { user: User }) { ... }
// src/features/product/components/ProductCard.tsx
export function ProductCard({ product }: { product: Product }) { ... }
// Clear imports
import { UserCard } from '@/features/user';
import { ProductCard } from '@/features/product';Naming patterns:
| Type | Pattern | Example |
|---|---|---|
| Feature component | {Feature}{Component} | UserProfile, CartSummary |
| Feature hook | use{Feature}{Action} | useUserAuth, useCartItems |
| Feature API | {action}{Feature} | getUser, updateCart |
| Feature store | use{Feature}Store | useCartStore, useUserStore |
Exception for shared components:
// Shared components can use generic names - they're not feature-specific
// src/shared/components/Button.tsx
export function Button({ children, ...props }) { ... }
// src/shared/components/Card.tsx
export function Card({ children, ...props }) { ... }Benefits:
- IDE autocomplete shows meaningful options
- Imports are self-documenting
- No aliasing required
Reference: React Naming Conventions
Use Domain-Driven Feature Names
Name features after business domains, not technical implementations. This makes the codebase navigable for non-developers and ensures feature boundaries align with business boundaries.
Incorrect (technical naming):
src/features/
├── data-grid/ # What data? What domain?
├── form-handler/ # What form? What entity?
├── api-client/ # Generic technical concern
├── modal-manager/ # UI pattern, not domain
└── list-view/ # Generic view patternCorrect (domain naming):
src/features/
├── user/ # User management domain
├── product/ # Product catalog domain
├── cart/ # Shopping cart domain
├── checkout/ # Checkout/payment domain
├── order/ # Order management domain
├── notification/ # Notification domain
└── search/ # Search domainNaming guidelines:
| Domain | Good Name | Bad Name |
|---|---|---|
| User management | user, account | profile-component |
| Product catalog | product, catalog | item-list |
| Shopping | cart, checkout | purchase-flow |
| Authentication | auth | login-system |
Sub-features:
src/features/
├── user/
│ ├── ... # Core user feature
├── user-preferences/ # Distinct sub-domain
└── user-notifications/ # Another sub-domainAsk these questions:
- Would a product manager understand this name?
- Does this map to a business capability?
- Would this name make sense in a requirements document?
Reference: Domain-Driven Design - Eric Evans
Use Consistent File Naming Conventions
Establish and follow consistent file naming patterns. This enables automated tooling, makes files predictable, and reduces decision fatigue.
Incorrect (inconsistent naming):
src/features/user/
├── components/
│ ├── UserProfile.tsx # PascalCase
│ ├── user-avatar.tsx # kebab-case
│ ├── userBadge.tsx # camelCase
│ └── User_Settings.tsx # Snake_Case
├── hooks/
│ ├── useUser.ts # camelCase
│ └── use-auth.ts # kebab-case
└── api/
├── getUser.ts # camelCase
└── user-api.ts # kebab-caseCorrect (consistent conventions):
src/features/user/
├── components/
│ ├── UserProfile.tsx # PascalCase for components
│ ├── UserAvatar.tsx
│ ├── UserBadge.tsx
│ └── UserSettings.tsx
├── hooks/
│ ├── useUser.ts # camelCase with use prefix
│ └── useUserAuth.ts
├── api/
│ ├── get-user.ts # kebab-case for non-components
│ ├── update-user.ts
│ └── delete-user.ts
├── stores/
│ └── user-store.ts # kebab-case
├── types/
│ └── index.ts
└── utils/
└── format-user-name.ts # kebab-caseRecommended conventions:
| File Type | Convention | Example |
|---|---|---|
| React components | PascalCase | UserProfile.tsx |
| Hooks | camelCase with use prefix | useUser.ts |
| API functions | kebab-case | get-user.ts |
| Stores | kebab-case | user-store.ts |
| Utilities | kebab-case | format-date.ts |
| Types | index.ts or kebab-case | types/index.ts |
| Tests | match source + .test | UserProfile.test.tsx |
ESLint enforcement:
// .eslintrc.js
rules: {
'unicorn/filename-case': ['error', {
cases: {
pascalCase: true, // For .tsx files
kebabCase: true, // For .ts files
},
}],
}Reference: Airbnb React Style Guide
Separate App Layer from Features
The app layer handles global concerns: routing, providers, initialization, and global layouts. Features should not contain routing logic or provider setup. This separation allows features to be portable and testable in isolation.
Incorrect (routing and providers mixed with features):
src/features/user/
├── components/
│ └── UserProfile.tsx
├── UserRoutes.tsx # Routing logic in feature
└── UserProvider.tsx # Provider in feature// src/features/user/UserRoutes.tsx
import { Routes, Route } from 'react-router-dom';
export function UserRoutes() {
return (
<Routes>
<Route path="/profile" element={<UserProfile />} />
<Route path="/settings" element={<UserSettings />} />
</Routes>
);
}Correct (app layer owns routing and providers):
src/
├── app/
│ ├── providers/
│ │ ├── AuthProvider.tsx
│ │ ├── QueryProvider.tsx
│ │ └── index.tsx
│ ├── routes/
│ │ ├── index.tsx
│ │ └── protected-routes.tsx
│ └── App.tsx
└── features/
└── user/
├── components/
│ ├── UserProfile.tsx
│ └── UserSettings.tsx
└── index.ts// src/app/routes/index.tsx
import { UserProfile, UserSettings } from '@/features/user';
export const routes = [
{ path: '/profile', element: <UserProfile /> },
{ path: '/settings', element: <UserSettings /> },
];App layer responsibilities:
- Route definitions and navigation
- Provider composition (Auth, Query, Theme)
- Global error boundaries
- Application initialization
Reference: Feature-Sliced Design
Organize by Feature, Not Technical Type
Technical grouping (components/, hooks/, utils/) forces developers to navigate multiple directories for single features. Feature-based organization colocates all related code, making features self-documenting and independently deployable.
Incorrect (technical grouping):
src/
├── components/
│ ├── PostCard.tsx
│ ├── CommentList.tsx
│ └── UserAvatar.tsx
├── hooks/
│ ├── usePost.ts
│ ├── useComments.ts
│ └── useUser.ts
├── api/
│ ├── posts.ts
│ ├── comments.ts
│ └── users.ts
└── utils/
├── postHelpers.ts
└── commentHelpers.tsCorrect (feature-based grouping):
src/
├── features/
│ ├── post/
│ │ ├── components/
│ │ │ └── PostCard.tsx
│ │ ├── hooks/
│ │ │ └── usePost.ts
│ │ ├── api/
│ │ │ └── get-post.ts
│ │ └── utils/
│ │ └── postHelpers.ts
│ ├── comment/
│ │ ├── components/
│ │ │ └── CommentList.tsx
│ │ ├── hooks/
│ │ │ └── useComments.ts
│ │ └── api/
│ │ └── get-comments.ts
│ └── user/
│ ├── components/
│ │ └── UserAvatar.tsx
│ └── hooks/
│ └── useUser.ts
└── shared/
└── components/
└── Button.tsxBenefits:
- Adding a feature = adding one folder
- Removing a feature = removing one folder
- Feature ownership is immediately clear
- Teams can work on different features without conflicts
Reference: Robin Wieruch - React Feature Architecture
Make Features Self-Contained
Each feature folder should contain everything needed to implement that feature. When a feature requires code from multiple places, it becomes entangled with other features and cannot evolve independently.
Incorrect (scattered feature code):
// src/components/checkout/CheckoutForm.tsx
import { useCart } from '../../hooks/useCart';
import { validateCard } from '../../utils/validation';
import { CartSummary } from '../cart/CartSummary';
import { paymentApi } from '../../api/payment';
export function CheckoutForm() {
const cart = useCart();
// Feature depends on 4 different locations
}Correct (self-contained feature):
// src/features/checkout/components/CheckoutForm.tsx
import { useCart } from '../hooks/useCart';
import { validateCard } from '../utils/validation';
import { CartSummary } from '../components/CartSummary';
import { submitPayment } from '../api/submit-payment';
export function CheckoutForm() {
const cart = useCart();
// All imports are within the feature
}Feature folder structure:
features/checkout/
├── api/
│ └── submit-payment.ts
├── components/
│ ├── CheckoutForm.tsx
│ └── CartSummary.tsx
├── hooks/
│ └── useCart.ts
├── utils/
│ └── validation.ts
└── index.tsWhen NOT to use this pattern:
- Truly generic utilities (date formatting, string helpers) belong in
shared/ - UI primitives (Button, Input) belong in
shared/components/
Reference: Bulletproof React - Project Structure
Keep Directory Hierarchy Flat
Deep nesting creates long import paths, makes file relocation difficult, and obscures the overall structure. Limit nesting to 2-3 levels within features.
Incorrect (deep nesting):
src/features/checkout/
├── components/
│ ├── form/
│ │ ├── fields/
│ │ │ ├── payment/
│ │ │ │ └── CardInput.tsx
│ │ │ └── shipping/
│ │ │ └── AddressInput.tsx
│ │ └── FormWrapper.tsx
│ └── summary/
│ └── OrderSummary.tsx// Import path is 6 levels deep
import { CardInput } from '../../../components/form/fields/payment/CardInput';Correct (flat hierarchy):
src/features/checkout/
├── components/
│ ├── CardInput.tsx
│ ├── AddressInput.tsx
│ ├── FormWrapper.tsx
│ └── OrderSummary.tsx// Import path is 2 levels
import { CardInput } from '../components/CardInput';When deeper nesting is acceptable:
- Feature has 20+ components (consider splitting into sub-features)
- Clear categorical distinction (e.g.,
forms/vsdisplays/)
Guidelines:
- Maximum 3 levels within a feature folder
- If you need deeper nesting, the feature is likely too large
- Prefer flat with clear naming over deep with vague naming
Reference: Robin Wieruch - React Folder Structure
Include Only Necessary Segments
Not every feature needs every segment (components/, hooks/, api/, utils/, types/). Start with only what the feature requires and add segments as complexity grows. Empty folders add noise and suggest over-engineering.
Incorrect (every segment even when unused):
src/features/notification/
├── api/ # Empty - notifications are client-side only
├── components/
│ └── Toast.tsx
├── hooks/
│ └── useNotification.ts
├── stores/ # Empty - using context instead
├── types/
│ └── index.ts # Just re-exports one interface
└── utils/ # EmptyCorrect (only necessary segments):
src/features/notification/
├── components/
│ └── Toast.tsx
├── hooks/
│ └── useNotification.ts
└── types.ts # Single file, not a folder with one fileAnother example - simple feature:
src/features/theme/
├── ThemeProvider.tsx
├── useTheme.ts
└── index.tsComplex feature with all segments:
src/features/checkout/
├── api/
│ ├── submit-order.ts
│ └── validate-address.ts
├── components/
│ ├── CheckoutForm.tsx
│ ├── PaymentSection.tsx
│ └── ShippingSection.tsx
├── hooks/
│ ├── useCheckout.ts
│ └── usePaymentMethods.ts
├── stores/
│ └── checkout-store.ts
├── types/
│ └── index.ts
├── utils/
│ └── validation.ts
└── index.tsGuideline: Add segments when you have 2+ files that would go there.
Reference: Bulletproof React - Project Structure
Related skills
FAQ
What does feature-arch do?
feature-arch: A skill for development.
When should I use feature-arch?
When you need to use feature-arch for development tasks, or when feature-arch: a skill for development.
What are the main capabilities?
feature-arch.