All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 16m0s
8.6 KiB
8.6 KiB
ADR-044: Frontend Feature Organization Pattern
Date: 2026-01-09
Status: Accepted
Implemented: 2026-01-09
Context
The React frontend has grown to include multiple distinct features:
- Flyer viewing and management
- Shopping list creation
- Budget tracking and charts
- Voice assistant
- User personalization
- Admin dashboard
Without clear organization, code becomes scattered across generic folders (/components, /hooks, /utils), making it hard to:
- Understand feature boundaries
- Find related code
- Refactor or remove features
- Onboard new developers
Decision
We will adopt a feature-based folder structure where each major feature is self-contained in its own directory under /features. Shared code lives in dedicated top-level folders.
Design Principles
- Colocation: Keep related code together (components, hooks, types, utils).
- Feature Independence: Features should minimize cross-dependencies.
- Shared Extraction: Only extract to shared folders when truly reused.
- Flat Within Features: Avoid deep nesting within feature folders.
Implementation Details
Directory Structure
src/
├── features/ # Feature modules
│ ├── flyer/ # Flyer viewing/management
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── types.ts
│ │ └── index.ts
│ ├── shopping/ # Shopping lists
│ │ ├── components/
│ │ ├── hooks/
│ │ └── index.ts
│ ├── charts/ # Budget/analytics charts
│ │ ├── components/
│ │ └── index.ts
│ ├── voice-assistant/ # Voice commands
│ │ ├── components/
│ │ └── index.ts
│ └── admin/ # Admin dashboard
│ ├── components/
│ └── index.ts
├── components/ # Shared UI components
│ ├── ui/ # Primitive components (Button, Input, etc.)
│ ├── layout/ # Layout components (Header, Footer, etc.)
│ └── common/ # Shared composite components
├── hooks/ # Shared hooks
│ ├── queries/ # TanStack Query hooks
│ ├── mutations/ # TanStack Mutation hooks
│ └── utils/ # Utility hooks (useDebounce, etc.)
├── providers/ # React context providers
│ ├── AppProviders.tsx
│ ├── UserDataProvider.tsx
│ └── FlyersProvider.tsx
├── pages/ # Route page components
├── services/ # API clients, external services
├── types/ # Shared TypeScript types
├── utils/ # Shared utility functions
└── lib/ # Third-party library wrappers
Feature Module Structure
Each feature follows a consistent internal structure:
features/flyer/
├── components/
│ ├── FlyerCard.tsx
│ ├── FlyerGrid.tsx
│ ├── FlyerUploader.tsx
│ ├── FlyerItemList.tsx
│ └── index.ts # Re-exports all components
├── hooks/
│ ├── useFlyerDetails.ts
│ ├── useFlyerUpload.ts
│ └── index.ts # Re-exports all hooks
├── types.ts # Feature-specific types
├── utils.ts # Feature-specific utilities
└── index.ts # Public API of the feature
Feature Index File
Each feature has an index.ts that defines its public API:
// features/flyer/index.ts
export { FlyerCard, FlyerGrid, FlyerUploader } from './components';
export { useFlyerDetails, useFlyerUpload } from './hooks';
export type { FlyerViewProps, FlyerUploadState } from './types';
Import Patterns
// Importing from a feature (preferred)
import { FlyerCard, useFlyerDetails } from '@/features/flyer';
// Importing shared components
import { Button, Card } from '@/components/ui';
import { useDebounce } from '@/hooks/utils';
// Avoid: reaching into feature internals
// import { FlyerCard } from '@/features/flyer/components/FlyerCard';
Provider Organization
Located in src/providers/:
// AppProviders.tsx - Composes all providers
export function AppProviders({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<AuthProvider>
<UserDataProvider>
<FlyersProvider>
<ThemeProvider>
{children}
</ThemeProvider>
</FlyersProvider>
</UserDataProvider>
</AuthProvider>
</QueryClientProvider>
);
}
Query/Mutation Hook Organization
Located in src/hooks/:
// hooks/queries/useFlyersQuery.ts
export function useFlyersQuery(options?: { storeId?: number }) {
return useQuery({
queryKey: ['flyers', options],
queryFn: () => flyerService.getFlyers(options),
staleTime: 5 * 60 * 1000,
});
}
// hooks/mutations/useFlyerUploadMutation.ts
export function useFlyerUploadMutation() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: flyerService.uploadFlyer,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['flyers'] });
},
});
}
Page Components
Pages are thin wrappers that compose feature components:
// pages/Flyers.tsx
import { FlyerGrid, FlyerUploader } from '@/features/flyer';
import { PageLayout } from '@/components/layout';
export function FliversPage() {
return (
<PageLayout title="My Flyers">
<FlyerUploader />
<FlyerGrid />
</PageLayout>
);
}
Cross-Feature Communication
When features need to communicate, use:
- Shared State Providers: For global state (user, theme).
- Query Invalidation: For data synchronization.
- Event Bus: For loose coupling (see ADR-036).
// Feature A triggers update
const uploadMutation = useFlyerUploadMutation();
await uploadMutation.mutateAsync(file);
// Query invalidation automatically updates Feature B's flyer list
Naming Conventions
| Item | Convention | Example |
|---|---|---|
| Feature folder | kebab-case | voice-assistant/ |
| Component file | PascalCase | FlyerCard.tsx |
| Hook file | camelCase with use |
useFlyerDetails.ts |
| Type file | lowercase | types.ts |
| Utility file | lowercase | utils.ts |
| Index file | lowercase | index.ts |
When to Create a New Feature
Create a new feature folder when:
- The functionality is distinct and self-contained.
- It has its own set of components, hooks, and potentially types.
- It could theoretically be extracted into a separate package.
- It has minimal dependencies on other features.
Do NOT create a feature folder for:
- A single reusable component (use
components/). - A single utility function (use
utils/). - A single hook (use
hooks/).
Consequences
Positive
- Discoverability: Easy to find all code related to a feature.
- Encapsulation: Features have clear boundaries and public APIs.
- Refactoring: Can modify or remove features with confidence.
- Scalability: Supports team growth with feature ownership.
- Testing: Can test features in isolation.
Negative
- Duplication Risk: Similar utilities might be duplicated across features.
- Decision Overhead: Must decide when to extract to shared folders.
- Import Verbosity: Feature imports can be longer.
Mitigation
- Regular refactoring sessions to extract shared code.
- Lint rules to prevent importing from feature internals.
- Code review focus on proper feature boundaries.
Key Directories
src/features/flyer/- Flyer viewing and managementsrc/features/shopping/- Shopping list functionalitysrc/features/charts/- Budget and analytics chartssrc/features/voice-assistant/- Voice command interfacesrc/features/admin/- Admin dashboardsrc/components/ui/- Shared primitive componentssrc/hooks/queries/- TanStack Query hookssrc/providers/- React context providers