Files
flyer-crawler.projectium.com/docs/adr/0044-frontend-feature-organization.md
Torben Sorensen e14c19c112
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 16m0s
linting docs + some fixes go claude and gemini
2026-01-09 22:38:57 -08:00

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:

  1. Understand feature boundaries
  2. Find related code
  3. Refactor or remove features
  4. 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:

  1. Shared State Providers: For global state (user, theme).
  2. Query Invalidation: For data synchronization.
  3. 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:

  1. The functionality is distinct and self-contained.
  2. It has its own set of components, hooks, and potentially types.
  3. It could theoretically be extracted into a separate package.
  4. 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 management
  • src/features/shopping/ - Shopping list functionality
  • src/features/charts/ - Budget and analytics charts
  • src/features/voice-assistant/ - Voice command interface
  • src/features/admin/ - Admin dashboard
  • src/components/ui/ - Shared primitive components
  • src/hooks/queries/ - TanStack Query hooks
  • src/providers/ - React context providers