Imported from incubateur-ademe/benefriches (
apps/web/AGENTS.md). Install upstream withnpx skills add incubateur-ademe/benefriches --skill web. Copyright stays with the author.
Benefriches Web App - Quick Reference
React SPA with Redux event-based architecture + Clean Architecture
⚠️ Redux is ESTABLISHED: This codebase uses Redux as an event-based architecture. Do NOT refactor to other state management solutions (Zustand, Jotai, etc.). Work with the existing patterns.
Quality Guards (ALWAYS RUN)
pnpm --filter web typecheck && pnpm --filter web lint && pnpm --filter web test && pnpm --filter web format:check
# Run a single test file
pnpm --filter web test path/to/file.spec.ts
If modifying shared package: Run pnpm --filter shared build first, then pnpm --filter web install, then web checks.
Architecture Rules
- Clean Architecture: Core has NO dependencies on infrastructure or views
- Dependency rule: Infrastructure and views depend on core, never the reverse
Canonical Pattern Examples
| Pattern | Reference File |
|---|---|
| Step Handler (registry) | src/features/create-project/core/renewable-energy/step-handlers/stepHandlerRegistry.ts |
| Step Handler (answer) | src/features/create-project/core/renewable-energy/step-handlers/photovoltaic/photovoltaic-surface/photovoltaicSurface.handler.ts |
| Step Handler (selector) | src/features/create-project/core/renewable-energy/step-handlers/photovoltaic/photovoltaic-contract-duration/photovoltaicContractDuration.selectors.ts |
| Step Handler (schema) | src/features/create-project/core/renewable-energy/step-handlers/photovoltaic/photovoltaic-surface/photovoltaicSurface.schema.ts |
| Step Handler (stepper config) | src/features/create-project/core/renewable-energy/step-handlers/photovoltaic/photovoltaic-surface/photovoltaicSurface.stepperConfig.ts |
| Stepper Config (registry) | src/features/create-project/core/renewable-energy/step-handlers/renewableEnergyStepperConfig.ts |
| Step Handler (urban project, with deps) | src/features/create-project/core/urban-project/step-handlers/uses/selection/usesSelection.handler.ts |
| Step Handler (urban project, registry) | src/features/create-project/core/urban-project/step-handlers/stepHandlerRegistry.ts |
| Step Handler (urban zone site, registry) | src/features/create-site/core/urban-zone/stepHandlerRegistry.ts |
| Test Store Helper (urban zone site) | src/features/create-site/core/urban-zone/__tests__/_testStoreHelpers.ts |
| ViewData Selector | src/features/create-project/core/createProject.selectors.ts |
| Async Thunk | src/features/create-project/core/urban-project/fetchEstimatedSiteResalePrice.action.ts |
| Reducer (createReducer) | src/features/create-site/core/createSite.reducer.ts |
| Container Component | src/features/create-project/views/photovoltaic-power-station/stakeholders/site-purchased/index.tsx |
| Gateway Interface | src/shared/core/gateways/RealEstateValuationGateway.ts |
| HTTP POST implementation | src/features/onboarding/infrastructure/create-user-service/HttpCreateUserService.ts |
| HTTP GET implementation | src/features/onboarding/infrastructure/current-user-service/HttpCurrentUserService.ts |
| InMemory Mock | src/shared/infrastructure/real-estate-valuation-service/InMemoryRealEstateValuationService.ts |
| Step Handler (test, RE) | src/features/create-project/core/renewable-energy/step-handlers/photovoltaic/photovoltaic-surface/photovoltaicSurface.step.spec.ts |
| Test Store Helper (RE) | src/features/create-project/core/renewable-energy/__tests__/_testStoreHelpers.ts |
| Test with Store Helper (urban) | src/features/create-project/core/urban-project/__tests__/steps/site-resale/siteResaleSelection.step.spec.ts |
| Test Store Helper (urban) | src/features/create-project/core/urban-project/__tests__/_testStoreHelpers.ts |
| Listener Middleware | src/features/create-project/core/listeners/projectCreationListeners.ts |
| Third-Party Gateway | src/features/support/core/gateways/SupportChatGateway.ts |
| Fire-and-forget Thunk | src/features/support/core/authLinkNotReceivedHelpRequested.action.ts |
Naming Conventions
- Actions: Passive tense (events):
stepCompleted,dataFetched(NOTcompleteStep,fetchData) - Selectors:
select{Feature}ViewData- one per container returning composed object
Key Patterns
Container/Presentational Separation
- Container (
index.tsx): Connects Redux, uses single ViewData selector, dispatches actions - Presentational: Receives all data via props, no Redux dependencies
ViewData Pattern
Container components access state through a single selector returning a composed ViewData object:
const dispatch = useAppDispatch();
const viewData = useAppSelector(selectFeatureViewData);
return <FeaturePage viewData={viewData} onAction={(data) => dispatch(actionCompleted(data))} />;
Gateway Pattern
- Interface in
core/gateways/- defines what domain needs - HTTP implementation in
infrastructure/*/Http*Service.ts- real API calls - InMemory mock in
infrastructure/*/InMemory*Service.ts- required for tests - Register in store via
extraArgumentfor thunk access
DTO Validation: HTTP services should validate request/response bodies using shared Zod schemas from "shared" with safeParse(). See src/features/onboarding/infrastructure/create-user-service/HttpCreateUserService.ts for reference.
Dependency Injection
Services injected via store's extraArgument, accessed in thunks as extra:
const result = await extra.featureService.doSomething(payload);
Testing
- Store-based unit tests (
.spec.ts):new StoreBuilder().withSiteData({...}).build()with InMemory services - Component tests (
.spec.tsx): Use@testing-library/reactfor DOM-level rendering tests
Side Effects
- Use
useEffectin components for most side effects - Use listener middleware sparingly for specific Redux action side effects
Step Handler Pattern
Multi-step wizards use a step handler registry instead of per-step actions and large reducers. Two implementations exist:
Common to both:
- Registry (
stepHandlerRegistry.ts): Maps step IDs to handler objects AnswerStepHandler<T>: Data-entry steps withgetNextStepId(), optionalgetDefaultAnswers(),updateAnswersMiddleware()InfoStepHandler: Navigation-only steps (intros, summaries)- Generic action:
stepCompletionRequested({ stepId, answers })replaces individual per-step actions - Colocated files: Each step has
*.handler.ts,*.schema.ts,*.selectors.ts,*.stepperConfig.ts
Renewable Energy (simpler, creation only — ADR-0006):
- Location:
src/features/create-project/core/renewable-energy/step-handlers/ - Nested per-step directories (e.g.,
photovoltaic/photovoltaic-surface/photovoltaicSurface.handler.ts) - Steps are independent (no cascading updates)
Urban Project (complex, creation + update — ADR-0004):
- Location:
src/features/create-project/core/urban-project/step-handlers/ - Nested per-step folders (e.g.,
uses/selection/usesSelection.handler.ts) - Dependency rules:
getDependencyRules()returnsdelete/invalidate/recomputeactions on dependent steps - Shortcuts:
getShortcut()auto-completes multiple steps when conditions are met - Recomputation:
getRecomputedStepAnswers()recalculates values while preserving user edits - Confirmation dialogs: Cascading changes trigger user confirmation before applying
- Factory actions:
createUrbanProjectFormActions(prefix)supports both"projectCreation"and"projectUpdate"modes
Component Discovery (lookup order)
Before creating a new UI component, always check these sources in order:
src/shared/views/components/— internal shared components (e.g.,RadioButtons,CheckableTile,BackNextButtons,Dialog,Spinner)@codegouvfr/react-dsfr— DSFR component library (buttons, inputs, badges, modals, etc.)- Only if neither has what you need: create a new component
Component Encapsulation
Internal representation is an implementation detail — don't expose it through props. Design component APIs from the consumer's perspective. If every consumer would need the same adapter code (format conversion, mapping, etc.), that logic belongs inside the component.
Creation Checklists
Container Component Checklist
When creating a new container component:
-
Create ViewData selector in the relevant selectors file:
- Define typed
{Feature}ViewDatatype with all data the container needs - Create
select{Feature}ViewDataselector composing data from state - Export the selector (directly, or via factory function for urban project forms)
- Define typed
-
Create container (
index.tsx):- Get selector via direct import or hook (
useProjectForm()for urban project forms) - Use single
useAppSelector(select{Feature}ViewData)call - Destructure ViewData and pass to presentational component
- Handle actions with
useAppDispatch
- Get selector via direct import or hook (
-
Reference examples (simplest first):
src/features/create-project/core/usecase-selection/useCaseSelection.selectors.ts→selectUseCaseCreateModeViewDatasrc/features/create-project/views/usecase-selection/create-mode-selection/index.tsxsrc/features/create-site/core/steps/spaces/spaces.selectors.ts→selectSiteSoilsSummaryViewDatasrc/features/create-site/views/common-views/spaces-and-soils/soils-summary/index.tsx- Urban project form (factory pattern):
src/features/create-project/core/urban-project/urbanProject.selectors.ts→selectUsesFloorSurfaceAreaViewDatasrc/features/create-project/views/urban-project/buildings/uses-floor-surface-area/index.tsx
Step Handler Checklist (Renewable Energy)
When adding a new step to the renewable energy wizard:
- Create step directory:
step-handlers/{group}/{step-id}/ - Create colocated files:
{stepId}.handler.ts— implementAnswerStepHandler<T>orInfoStepHandler{stepId}.schema.ts— Zod schema for step answers (ifAnswerStepHandler){stepId}.selectors.ts— selector returning stepViewData{stepId}.stepperConfig.ts— label + optional group for the stepper UI
- Register in registry: Add to
step-handlers/stepHandlerRegistry.ts - Add step ID to
renewableEnergySteps.tsunion type - Write colocated test:
{stepId}.step.spec.tsusingStoreBuilderfrom__tests__/_testStoreHelpers.ts
Gateway Checklist
When adding a new external service integration:
- Create interface in
core/gateways/- define methods the domain needs - Create HTTP implementation in
infrastructure/*/Http*Service.ts- real API calls - Create InMemory mock in
infrastructure/*/InMemory*Service.ts- required for tests - Register in store via
extraArgumentfor thunk access
Third-Party Service Integration
Third-party services (Crisp, analytics SDKs, etc.) belong in the infrastructure layer, not as React components.
- Define gateway interface in
core/gateways/ - Implement in
infrastructure/(real + InMemory + Noop) - Register in
AppDependencies(not React context) - Views dispatch thunks that call
extra.service— no direct SDK imports in views - Reference:
features/support/(Crisp chat),features/analytics/(Matomo analytics)
Import Conventions
| Import Type | Pattern | Example |
|---|---|---|
| Within web app | @/ alias |
import { useAppSelector } from "@/app/hooks/store.hooks" |
| From shared package | shared |
import type { GetSiteViewResponseDto } from "shared" |
| Relative | ./ or ../ |
Only within same feature folder |
Critical DON'Ts
- Don't import infrastructure in core (violates Clean Architecture)
- Don't put selectors in
views/— selectors are core logic; always place incore/(Clean Architecture dependency rule) - Don't call multiple selectors in containers (compose into single ViewData selector)
- Don't skip InMemory implementations (required for tests)
- Don't use untyped Redux hooks - always use
useAppSelector/useAppDispatchfrom@/app/hooks/store.hooks
Feature Structure
App-Level (Composition Root)
app/ # Composition root — app bootstrap & wiring
├── App.tsx # Root component (route dispatch)
├── envVars.ts # Environment variables
├── router.ts # Route definitions (type-route)
├── hooks/
│ └── store.hooks.ts # Typed useAppSelector/useAppDispatch
└── store/
├── store.ts # createStore + AppDependencies type + RootState/AppDispatch
├── rootReducer.ts # Combined reducer
├── appDependencies.ts # Production dependency wiring
├── appAsyncThunk.ts # Typed createAsyncThunk
└── listenerMiddleware.ts # Listener middleware setup
Feature-Level
feature-name/
├── core/ # Business logic (preferred name for new features)
│ ├── feature.types.ts # Type definitions (single source of truth)
│ ├── featureName.reducer.ts # Reducer using createReducer
│ ├── featureName.selectors.ts # Selectors including ViewData
│ ├── actions/*.ts # Action creators (passive tense)
│ └── __tests__/*.spec.ts # Unit tests
├── infrastructure/
│ └── feature-service/
│ ├── HttpFeatureService.ts # HTTP implementation
│ └── InMemoryFeatureService.ts # Test mock (required)
└── views/
├── index.tsx # Container (Redux-connected)
└── FeaturePage.tsx # Presentational component
Note: Some older features use application/ instead of core/ (e.g., my-evaluations, projects). Use core/ for new features — application/ is legacy naming.
Tech Stack
React 19+, Redux Toolkit 2+, Vite 7+, TypeScript 5+ (strict), Tailwind CSS v4 (uses @import "tailwindcss/..." syntax, not v3 @tailwind directives) + DSFR, type-route, react-hook-form, Highcharts (impact charts), @react-pdf/renderer (PDF export)
Related Documentation
- Monorepo Guide: AGENTS.md
- API Guide: apps/api/AGENTS.md
- Feature Example: docs/feature-example.md
- React Best Practices: .claude/skills/react-best-practices/SKILL.md