# Frontend — Patterns : State > Extrait de la base de connaissance Lead_tech. Voir `knowledge/frontend/patterns/README.md` pour l'index complet. --- ## Pattern : Gestion explicite des états UI (loading / empty / error) ### Synthèse - **Objectif** : éviter les interfaces ambiguës ou incohérentes en rendant explicites tous les états possibles d'une vue. - **Contexte** : SPA ou webapp consommant des données asynchrones (API, backend, cache). - **Quand l'utiliser** : dès qu'une vue dépend de données externes ou d'un traitement async. - **Quand l'éviter** : vues purement statiques ou synchrones sans dépendance externe. ### Analyse - **Avantages** : - UX plus prévisible et compréhensible - Debug facilité (état visible = problème identifiable) - Base saine pour tests et accessibilité - **Limites / vigilance** : - Peut sembler verbeux sur des écrans simples - Nécessite une discipline pour ne pas "court-circuiter" les états ### Validation - Validé le : 25-01-2026 - Contexte technique : SPA (React / Vue / Svelte agnostique), API HTTP ### Implémentation (exemple minimal) ```txt if (loading) { afficher un skeleton ou spinner } else if (error) { afficher un message clair + action possible } else if (data est vide) { afficher un état empty explicite } else { afficher la vue nominale } ``` ### Checklist - [ ] Aucun écran blanc ou silencieux - [ ] Message d'erreur compréhensible pour l'utilisateur - [ ] États testables individuellement - [ ] Accessibilité respectée (focus, lecture écran) - [ ] Pas de logique métier cachée dans le rendu --- ## Pattern : Séparation claire server state / client state ### Synthèse - **Objectif** : éviter le mélange des responsabilités entre données serveur et état local UI. - **Contexte** : SPA ou webapp consommant une API avec interactions utilisateur. - **Quand l'utiliser** : dès que l'application affiche des données distantes modifiables ou synchronisées. - **Quand l'éviter** : applications très simples ou purement statiques. ### Analyse - **Avantages** : - Logique plus lisible et testable - Réduction des bugs liés aux états incohérents - Évolutivité facilitée quand l'app grossit - **Limites / vigilance** : - Demande de la rigueur dans le découpage - Peut sembler abstrait au début pour des petits projets ### Validation - Validé le : 25-01-2026 - Contexte technique : SPA agnostique (React / Vue / Svelte), API HTTP ### Implémentation (exemple minimal) ```txt serverState = données venant du backend (fetch, cache, sync) clientState = état local UI (filtres, onglets, modales, formulaires) Ne jamais : - stocker du state UI dans le cache serveur - dériver la logique UI directement des réponses API sans adaptation ``` ### Checklist - [ ] Les données serveur peuvent être invalidées / rechargées - [ ] L'état UI est local et réinitialisable - [ ] Les responsabilités sont lisibles dans le code - [ ] Les tests peuvent cibler chaque type d'état - [ ] Pas de dépendance implicite entre UI et API --- ## Pattern : Refresh idempotent sur store de liste paginée ### Synthèse - **Objectif** : garantir qu'un pull-to-refresh recharge une liste paginée sans doublons, sans courses réseau et sans état intermédiaire incohérent. - **Contexte** : app mobile ou SPA avec store de domaine (ex. Zustand) et pagination incrémentale. - **Quand l'utiliser** : dès qu'une même liste supporte à la fois `loadMore` et `refresh`. - **Quand l'éviter** : listes purement statiques ou données entièrement remplacées sans pagination. ### Analyse - **Avantages** : - évite les doublons lors des refresh concurrents - garde une transition atomique entre ancien et nouvel état - rend le comportement async testable côté store - **Limites / vigilance** : - impose une discipline claire entre `refresh` et `loadMore` - demande une clé d'identité stable pour dédupliquer les items ### Validation - Validé le : 10-03-2026 - Contexte technique : React Native / Expo / Zustand / listes paginées ### Implémentation (exemple minimal) ```txt - conserver une promesse de refresh partagée tant qu'un refresh est en vol - refuser ou réutiliser tout refresh concurrent au lieu d'en lancer un second - remplacer atomiquement la liste à la fin du refresh - dédupliquer les items par identifiant au merge des pages suivantes - empêcher `loadMore` de fusionner sur un snapshot devenu obsolète ``` ### Checklist - [ ] Une seule promesse de refresh en vol à la fois - [ ] `refresh` et `loadMore` ont des garde-fous explicites - [ ] La liste est remplacée atomiquement après refresh - [ ] Les pages suivantes sont dédupliquées par identifiant stable - [ ] Tests sur refresh concurrent + refresh suivi de pagination --- ## Pattern : UI admin légère sur domaine existant ### Synthèse - **Objectif** : ajouter une capacité interne simple sans ouvrir trop tôt un back-office séparé ni dupliquer la logique métier. - **Contexte** : app mobile ou SPA avec un domaine métier déjà structuré et quelques actions internes ponctuelles. - **Quand l'utiliser** : publication, activation, modération légère, bascule de statut, action opérateur simple. - **Quand l'éviter** : permissions complexes, workflows multiples, audit riche ou volume d'actions qui justifie un vrai espace d'administration. ### Analyse - **Avantages** : - réutilise le service et le store métier existants - limite le coût de structure pour une capacité admin mince - garde les mutations testables et lisibles - **Limites / vigilance** : - ne pas laisser cette approche dériver vers un pseudo back-office implicite - le refresh après mutation doit être explicite sur les vues impactées ### Validation - Validé le : 10-03-2026 - Contexte technique : React Native / Expo Router / store de domaine ### Implémentation (exemple minimal) ```txt - ajouter une route dédiée minimale pour l'action interne - réutiliser le service/store métier existant au lieu de créer une couche parallèle - afficher le statut courant avant action - bloquer les actions concurrentes avec un flag explicite (`isUpdating*`) - déclencher un refresh explicite des vues impactées après succès - éviter les mutations en fire-and-forget ``` ### Checklist - [ ] Route dédiée minimale, pas de mini back-office générique - [ ] Réutilisation du domaine métier existant - [ ] Garde-fou explicite contre les doubles actions - [ ] Refresh explicite après mutation réussie - [ ] Tests sur succès, erreur et action concurrente --- ## Pattern : Hydratation auth asynchrone avec états explicites ### Synthèse Lors d'un passage sync -> async au boot, exposer un statut d'hydratation explicite (`idle`/`hydrating`/`ready`). ### Analyse Sans état transitoire formel, les guards lisent des valeurs incomplètes et déclenchent des redirections erronées. ### Validation - Validé le : 18-04-2026 - Contexte technique : Vue 3 / Pinia / boot async — RL799_V2 - Applicable à toute initialisation critique : session, feature flags, restore état persistant. ### Implémentation (exemple minimal) - Store : `hydrateStatus` + promesse partagée en cours pour les appels concurrents. - Guards : `await hydrate()` avant toute décision d'accès. - UI : fallback de rendu tant que l'hydratation n'est pas `ready`. --- ## Pattern : Refactor monolithe Vue — sous-lots Go/No-Go + ordre topologique ### Synthèse - **Objectif** : découper un composant Vue monolithique (> 1500 lignes script ou > 2000 lignes total) en composables + sous-composants livrés en commits successifs validés un à un. - **Contexte** : page Vue avec plusieurs responsabilités métier mêlées, peu ou pas de `describe`, sans sous-découpage interne. - **Quand l'utiliser** : fichier > 1500 lignes script, fenêtre de calme sans PR concurrente prévue, tests E2E robustes en place. - **Quand l'éviter** : page < 1000 lignes, pas de tests E2E pour servir de filet, ou planning serré sans Go/No-Go possible entre commits. ### Analyse - **Avantages** : - aucune régression à chaque sous-lot validé indépendamment - helpers réutilisables émergent naturellement - reporter vitest / Playwright groupe les échecs par responsabilité - **Limites / vigilance** : - les `data-testid` E2E doivent être **copiés-collés exactement** dans les composants enfants (sinon les E2E rotent silencieusement) - les bindings template doivent rester alignés avec les noms destructurés du composable - le typecheck `tsc --noEmit` ne suffit pas — utiliser `vue-tsc` (cf. `frontend/risques/state.md` risque-templates-vue-references-orphelines) ### Validation - Validé le : 29-04-2026 - Contexte technique : Vue 3 / Composition API — RL799_V2 (4 pages refactorées, 17 commits, 644/644 tests verts) ### Stratégie en 3 étapes 1. **Audit complet préalable** : sections du template avec plages de lignes + rôle métier + `v-if` clé, blocs script regroupés par responsabilité avec évaluation `autonome` vs `couplé`, imports + composables + DTOs consommés, tests existants, couplages externes (deep-links, sélecteurs CSS). 2. **Plan en sous-lots ordonnés par risque croissant** : - L1 : composables purement autonomes (zéro dépendance interne) - L2 : composants enfants auto-contenus (modales) - L3 : composables avec couplage modéré (cache + watchers) - L4 : composables avec couplage métier subtil (cascade, propagation) - L5 : composant enfant complexe (D&D, drag-handle conditionnel) 3. **Go/No-Go explicite entre chaque lot** : 1 commit thématique par lot avec validation typecheck + tests + diff montré au pair avant push. ### Ordre topologique des dépendances dans le script post-refactor Quand plusieurs composables se consomment mutuellement, respecter strictement l'ordre topologique (la déclaration de la donnée doit précéder son usage, sous peine de TDZ) : ```typescript // 1. Data centrale de la page const currentSoiree = ref(null); const error = ref(''); // 2. Composables qui ne consomment que la data centrale const { lifecycle, isLiveView } = useSoireeLifecycle(currentSoiree, allTenuesCancelled); // 3. Composables qui produisent des refs consommées par d'autres const { responsesData } = useResponseTracking(currentSoiree, error); // 4. Composables qui consomment les refs produites const { pastActiveGrade } = useGradeSelection(soireeTenues, responsesData); ``` ### Anti-patterns - ❌ Grouper plusieurs préoccupations dans un même composable juste parce qu'elles sont voisines dans le script - ❌ Sortir un composable qui consomme directement `process.cwd()` ou un store global sans le passer en argument (couplage caché) - ❌ Extraire le CSS scoped vers le composant enfant **avant** de vérifier que toutes les classes y sont effectivement utilisées (certaines classes vivent en CSS global) - ❌ Sauter le grep des références orphelines avant de supprimer un bloc ### Checklist - [ ] `data-testid` copiés-collés exactement dans les composants enfants - [ ] Bindings template alignés avec les noms destructurés - [ ] Props/events des composants enfants alignés avec les usages - [ ] `vue-tsc` (pas `tsc`) en vérification typecheck - [ ] QA visuel obligatoire post-refactor (mount réel en browser) --- ## Pattern : Convention `pages//{composables,components,utils,__tests__}/` ### Synthèse - **Objectif** : structurer une app Vue qui dépasse 20 pages avec plusieurs domaines métier, en regroupant la logique extraite par module. - **Contexte** : app Vue avec routing par page et logique extraite (composables, sous-composants, tests). - **Quand l'utiliser** : page > 1000 lignes envisage le scope, > 1500 lignes le crée systématiquement. - **Quand l'éviter** : page < 500 lignes sans logique extraite — laisser au niveau racine. ### Analyse - **Avantages** : - un module métier = un sous-dossier, navigation simplifiée - l'alias `@/pages//` rend les fichiers résilients aux déplacements - les tests d'un module vivent avec lui (pas dans un dossier global qui mélange tout) - **Limites / vigilance** : - les tests scopés calculent leur `root` avec **4 niveaux** de remontée (`'../../../..'`), pas 3 — source d'erreur fréquente lors d'un déplacement - le dossier `pages/__tests__/` global reste réservé aux tests transverses + tests des pages legacy ### Validation - Validé le : 29-04-2026 - Contexte technique : Vue 3 / Vite / Vitest — RL799_V2 (5 modules scopés, 35 fichiers extraits) ### Structure type ``` pages// ├── Page.vue # page principale (carcasse + template) ├── Page.vue # autres pages du même module si existent ├── composables/ # logique métier extraite │ ├── use.ts │ └── use.ts ├── components/ # sous-composants .vue scopés au module ├── utils/ # helpers purs (formatters, defensive wrappers) ├── styles.css # CSS partagé non-scoped (cf. pattern dédié) └── __tests__/ # tests scopés au module ``` ### Ce qui reste dans `pages/__tests__/` global Trois cas légitimes uniquement : 1. **Tests transverses** qui couvrent plusieurs modules (`lifecycleUnification.test.mjs`) 2. **Tests d'infrastructure** non rattachés à un module métier (`OfflineIntegration.test.mjs` pour le SW PWA) 3. **Tests des pages encore à plat** (legacy non-encore scopées) — `LoginPage.test.mjs` reste à `pages/__tests__/` tant que `LoginPage.vue` est à `pages/` ### Calcul de `root` dans les tests scopés ```typescript const here = dirname(fileURLToPath(import.meta.url)); // 4 niveaux : __tests__ → → pages → src → frontend const root = resolve(here, '../../../..'); ``` Si le test crashe avec `ENOENT: no such file … '/src/src/pages/...'`, c'est que le `root` n'a pas été ajusté. ### Imports : alias `@/` plutôt que relatif Toujours utiliser l'alias `@/pages//` plutôt que `./X` ou `../X`. Bénéfice : déplacer un fichier ne casse pas ses imports internes (juste les imports depuis l'extérieur, qu'on met à jour via sed bulk). ### Critère extraction composable vs composant | Cas | Préférer composable | Préférer composant | |-----|---------------------|---------------------| | Logique pure (state + actions, pas de markup) | ✓ | | | Modale auto-contenue, > 30 lignes template | | ✓ | | Form > 50 lignes avec validation | | ✓ | | Plusieurs refs/computeds entrelacés | ✓ | | | CSS spécifique > 50 lignes | | ✓ (avec styles.css si partagé) | Préférence générale : composables script-only quand possible (risque CSS nul, plus simple à tester). --- ## Pattern : `styles.css` partagé non-scoped pour modules avec composants extraits ### Synthèse - **Objectif** : partager des classes CSS entre la page parente et ses sous-composants extraits sans dupliquer le CSS dans chaque ` ``` Les composants enfants utilisent les classes sans rien importer : ```vue ``` ### Quoi mettre dans `styles.css` - Classes utilisées par > 1 composant enfant du module - Classes utilisées par la page ET par un composant enfant - Conventions/tokens visuels propres au module ### Quoi NE PAS y mettre - Classes utilisées uniquement dans la page parente → `