# Patterns front-end validés Ce fichier contient **uniquement** des patterns front-end : - testés, - validés, - utilisés dans des projets réels (ou des apps complètes, pas des snippets isolés). Il sert de **mémoire durable** pour éviter : - de refaire les mêmes erreurs, - de redélibérer éternellement sur des sujets déjà tranchés, - de propager des “bonnes pratiques” théoriques non éprouvées. Dernière mise à jour : 23-03-2026 --- ## Index - [Gestion explicite des états UI (loading / empty / error)](#pattern-etats-ui-loading-empty-error) - [Séparation claire server state / client state](#pattern-separation-server-state-client-state) - [Formulaire robuste avec validation et erreurs explicites](#pattern-formulaire-robuste) - [Navigation réactive post-action async (React / Expo Router)](#pattern-navigation-reactive-post-action-async) - [Refresh idempotent sur store de liste paginée](#pattern-refresh-idempotent-liste-paginee) - [UI admin légère sur domaine existant](#pattern-ui-admin-legere-domaine-existant) - [Intégration tierce en mode link-out — préférer une page locale canonique](#pattern-link-out-page-locale-canonique) - [Design Tokens natifs TypeScript (Expo / React Native)](#pattern-design-tokens-expo-rn) - [Tests de styles React Native sans renderer JSX](#pattern-tests-styles-sans-renderer) - [Export des styles de composant pour réutilisation partielle](#pattern-export-styles-composant) - [Token typography par usage sémantique (React Native)](#pattern-token-typography-semantique) - [Click-to-load strict pour les embeds tiers (iframe/widget)](#pattern-click-to-load-embeds-tiers) - [Toggle optimiste avec rollback (React Server Action)](#pattern-toggle-optimiste-rollback) - [Server Action retournant l'entité — élimination de `router.refresh()` sur create/edit](#pattern-server-action-retourne-entite) - [ESLint flat config avec presets Next.js (`eslint.config.mjs`)](#pattern-eslint-flat-config-nextjs) - [Grilles 2 colonnes FR/EN — mobile-first](#pattern-grilles-2-colonnes-mobile-first) --- ## Règle d’or Si ce n’est pas **confirmé comme fonctionnel et utile**, **ça n’a rien à faire ici**. - Pas de conseils vagues - Pas de patterns “à la mode” - Pas de dépendance implicite à un framework ou une version non précisée --- ## Périmètre couvert - SPA et webapps - UX technique (forms, erreurs, loading, feedback) - State management (client / server) - Architecture front-end - Performance et accessibilité - Sécurité front (au niveau applicatif) - DX et maintenabilité Ce fichier traite le **front-end comme un logiciel en production**, au même niveau d’exigence que le backend. --- ## Format standard d’un pattern (obligatoire) ## Pattern : - Objectif : ce que le pattern résout - Contexte : type d’application / contraintes - Quand l’utiliser : cas pertinents - Quand l’éviter : contre-exemples - Avantages : bénéfices concrets - Limites / vigilance : pièges, dette potentielle - Validé le : DD-MM-YYYY - Contexte technique : framework + version + tooling principal ### Implémentation (exemple minimal) ```txt (contenu volontairement minimal, lisible, non-magique) ``` --- ## 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 : Formulaire robuste avec validation et erreurs explicites ### Synthèse - **Objectif** : garantir des formulaires fiables, compréhensibles et maintenables. - **Contexte** : toute interface avec saisie utilisateur et règles métier. - **Quand l’utiliser** : dès qu’un formulaire dépasse un simple champ isolé. - **Quand l’éviter** : formulaires ultra-simples sans validation réelle. ### Analyse - **Avantages** : - UX claire (l’utilisateur sait quoi corriger) - Moins d’erreurs silencieuses - Base saine pour tests et accessibilité - **Limites / vigilance** : - Peut sembler verbeux sans discipline - Risque de duplication si mal factorisé ### Validation - Validé le : 25-01-2026 - Contexte technique : Front-end agnostique, API HTTP ### Implémentation (exemple minimal) ```txt - Validation côté client (format, champs requis) - Validation côté serveur (règles métier) - Mapping explicite des erreurs serveur → champs UI - Aucun submit silencieux ``` ### Checklist - [ ] Messages d’erreur compréhensibles et localisés - [ ] Validation client + serveur cohérente - [ ] Focus automatique sur le champ en erreur - [ ] États loading / disabled gérés - [ ] Tests sur cas valides et invalides --- ## Pattern : Navigation réactive post-action async (React / Expo Router) ### Synthèse - **Objectif** : déclencher la navigation après une action asynchrone (login, register, submit) de façon idiomatique et sans bypasser la réactivité React. - **Contexte** : SPA ou app mobile React avec state management (Zustand, Redux, Context) et router déclaratif (React Router, Expo Router, Next.js App Router). - **Quand l'utiliser** : dès qu'une navigation dépend du résultat d'une action async. - **Quand l'éviter** : navigations synchrones sans état async impliqué. ### Analyse - **Avantages** : - Respecte le cycle de vie React (pas de lecture de state hors cycle) - Re-render automatique si l'état change entre-temps - Testable : on peut assert sur l'état, pas sur des effets de bord - **Limites / vigilance** : - Ne pas oublier les dépendances du `useEffect` (ESLint react-hooks/exhaustive-deps) - Gérer le cas "composant démonté" si la navigation peut être annulée ### Validation - Validé le : 07-03-2026 - Contexte technique : React 18+ / Zustand / Expo Router — pattern applicable sur React Router, Next.js App Router ### Implémentation (exemple minimal) ```typescript // ❌ Anti-pattern : lecture de state hors cycle React const handleSubmit = async () => { await login(email, password); const { accessToken } = useAuthStore.getState(); // bypasse la réactivité if (accessToken) router.replace('/(tabs)'); }; // ✅ Pattern correct : useEffect réactif sur le state const { accessToken, isLoading, error } = useAuthStore(); useEffect(() => { if (accessToken && !isLoading && !error) { router.replace('/(tabs)'); } }, [accessToken, isLoading, error]); const handleSubmit = async () => { await login(email, password); // la navigation se déclenche via useEffect quand le store se met à jour }; ``` ### Pour les callbacks OAuth (ref nécessaire) ```typescript // Quand un callback externe déclenche la navigation const pendingOAuth = useRef(false); useEffect(() => { if (pendingOAuth.current && accessToken) { pendingOAuth.current = false; router.replace('/(tabs)'); } }, [accessToken]); const handleOAuth = async () => { pendingOAuth.current = true; await exchangeWithIdp(token); }; ``` ### Checklist - [ ] Aucun `store.getState()` utilisé pour lire l'état post-action dans un handler - [ ] `useEffect` avec dépendances explicites (state pertinent + isLoading + error) - [ ] Cas d'erreur géré (ne pas naviguer si error est défini) - [ ] `useRef` si le trigger vient d'un callback externe (OAuth, deep link) - [ ] Convention documentée dans la story foundations / project-context avant les premiers écrans --- ## 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 : Intégration tierce en mode link-out — préférer une page locale canonique ### Synthèse - **Objectif** : éviter les parcours concurrents et centraliser les garde-fous UX quand une fonctionnalité publique dépend d’un service tiers externe. - **Contexte** : site ou webapp avec CTA publics menant vers un tiers de réservation, paiement, prise de rendez-vous ou formulaire externe. - **Quand l’utiliser** : dès qu’une fonctionnalité externe dispose d’une page locale dédiée côté produit (`/reservation`, `/booking`, etc.). - **Quand l’éviter** : si le produit assume volontairement une sortie directe unique vers le tiers, sans page locale intermédiaire ni besoin de contextualisation. ### Analyse - **Avantages** : - UX plus cohérente entre home, navigation et pages dédiées - garde-fous, wording et fallbacks centralisés au même endroit - facilite l’évolution future vers embed, click-to-load ou variantes de parcours - **Limites / vigilance** : - ajoute une étape intermédiaire si la page locale n’apporte aucune valeur - demande de maintenir l’alignement entre les CTA internes et le parcours canonique ### Validation - Validé le : 19-03-2026 - Contexte technique : site web public / intégration tierce en mode lien externe ### Implémentation (exemple minimal) ```txt - faire pointer les CTA internes (home, nav, landing) vers une page locale dédiée - faire de cette page locale le point canonique vers le service tiers externe - centraliser sur cette page le contexte utile, les garde-fous et les fallbacks - éviter les sorties directes concurrentes vers le tiers depuis plusieurs endroits du site ``` ### Checklist - [ ] Un parcours canonique unique est défini - [ ] Les CTA internes convergent vers la page locale dédiée - [ ] Les garde-fous et fallbacks sont centralisés - [ ] Les sorties directes concurrentes vers le tiers sont évitées ou justifiées --- ## Pattern : Design Tokens natifs TypeScript (Expo / React Native) ### Synthèse - **Objectif** : centraliser les tokens de design sans librairie externe (NativeBase, Tamagui), typés et barrel-exportés. - **Contexte** : app Expo / React Native avec un système de design à maintenir. - **Quand l’utiliser** : dès le début d’un projet mobile, avant les premiers composants. - **Quand l’éviter** : si une librairie UI opinionée est déjà choisie et gère ses propres tokens. ### Analyse - **Avantages** : - aucune dépendance externe, zéro configuration magique - autocomplétion TypeScript exacte via `as const` + types dérivés - facile à migrer vers un design system plus élaboré ultérieurement - **Limites / vigilance** : - les fichiers TTF doivent être présents dans `assets/fonts/` — Google Fonts ne peut pas être téléchargé automatiquement, documenter comme pré-requis dans la story - ne pas réutiliser les tokens `spacing` pour les dimensions de composants (voir risques) ### Validation - Validé le : 19-03-2026 - Contexte technique : Expo SDK 52+ / React Native / TypeScript — app-alexandrie story 0.1 ### Implémentation (exemple minimal) ```typescript // apps/mobile/src/theme/colors.ts export const colors = { primary: ‘#2563EB’, error: ‘#DC2626’, // ... } as const; export type ColorToken = keyof typeof colors; // apps/mobile/src/theme/spacing.ts export const spacing = { xs: 4, sm: 8, md: 12, base: 16, lg: 24 } as const; export type SpacingToken = keyof typeof spacing; // apps/mobile/src/theme/index.ts (barrel export) export * from ‘./colors’; export * from ‘./spacing’; export * from ‘./typography’; export * from ‘./shadows’; ``` ### Checklist - [ ] Tous les tokens `as const` pour inférence exacte - [ ] Pas de Context React — constantes TypeScript pures - [ ] Types dérivés (`ColorToken = keyof typeof colors`) pour l’autocomplétion - [ ] `useFonts` dans `_layout.tsx` avec guard `!fontsLoaded` - [ ] Fichiers TTF présents dans `assets/fonts/` et documentés dans la story --- ## Pattern : Tests de styles React Native sans renderer JSX ### Synthèse - **Objectif** : tester les tokens et styles de composants React Native dans un environnement Jest `testEnvironment: node` sans renderer JSX. - **Contexte** : config Jest avec `transform: { ‘^.+\\.ts$’: ‘ts-jest’ }` — les `.tsx` ne sont pas transformés. - **Quand l’utiliser** : tokens de thème, logique pure, valeurs de style exportées. - **Quand l’éviter** : rendu conditionnel (styles dynamiques inline) — nécessite `@testing-library/react-native`. ### Analyse - **Avantages** : - teste que le composant utilise les bons tokens, pas seulement que les tokens ont des valeurs - détecte les régressions de style sans renderer - rapide, aucune config Jest supplémentaire - **Limites / vigilance** : - ne teste pas le style calculé au runtime (style conditionnel dynamique) ### Validation - Validé le : 19-03-2026 - Contexte technique : React Native / Jest / ts-jest — app-alexandrie story 0.2 ### Implémentation ```typescript // Button.tsx — exporter le StyleSheet avec un nom préfixé export const buttonStyles = StyleSheet.create({ base: { borderRadius: 20, height: 57 }, primary: { backgroundColor: colors.primary }, }); export function Button(...) { ... } // ui-components.spec.ts — importer et vérifier les tokens import { buttonStyles } from ‘./Button’; import { colors } from ‘@/theme’; it(‘variante primary utilise colors.primary’, () => { expect(buttonStyles.primary.backgroundColor).toBe(colors.primary); }); ``` ### Deux niveaux de tests UI recommandés 1. `.spec.ts` (node) : tokens, valeurs, logique pure 2. `.spec.tsx` (config séparée avec renderer) : rendu visuel, interactions --- ## Pattern : Export des styles de composant pour réutilisation partielle (React Native) ### Synthèse - **Objectif** : partager les dimensions et formes d’un composant UI vers des éléments custom qui en dérivent, sans dupliquer les valeurs. - **Contexte** : app React Native où des screens construisent des éléments qui doivent être "au gabarit" d’un composant existant. - **Quand l’utiliser** : bouton custom OAuth, container calqué sur un composant de base, etc. - **Quand l’éviter** : si l’écart visuel est intentionnel — dans ce cas, une constante locale est plus claire. ### Analyse - **Avantages** : - zéro drift silencieux : si les dimensions du composant changent, tous les éléments dérivés suivent - tests de styles possibles en dehors du composant - **Limites / vigilance** : - à n’utiliser que pour des éléments vraiment dérivés, pas comme contournement de design system ### Validation - Validé le : 19-03-2026 - Contexte technique : React Native / StyleSheet — app-alexandrie story 0.3 ### Implémentation ```typescript // Button.tsx export const buttonStyles = StyleSheet.create({ base: { borderRadius: 20, height: 57 }, primary: { backgroundColor: colors.primary }, }); export function Button(...) { ... } // login.tsx — bouton OAuth au gabarit du Button import { buttonStyles } from ‘@/components/ui/Button’; ``` --- ## Pattern : Token typography par usage sémantique (React Native) ### Synthèse - **Objectif** : éviter les mauvais usages de tokens typography visuellement proches mais sémantiquement distincts. - **Contexte** : fichier `typography.ts` dans un design system React Native. - **Quand l’utiliser** : dès que deux tokens partagent la même taille mais un poids différent. - **Quand l’éviter** : jamais — les tokens typography doivent toujours refléter l’usage, pas l’apparence. ### Analyse - **Avantages** : - prévient les "approximations" de tokens en code review - changement de style d’usage spécifique sans régression globale - **Limites / vigilance** : - en review : chercher les usages sans `fontWeight` explicite — c’est souvent le signe que le mauvais token a été choisi ### Validation - Validé le : 19-03-2026 - Contexte technique : React Native / TypeScript — app-alexandrie story 0.4 ### Implémentation ```typescript // Bon : nommé par usage sémantique listItemTitle: { fontSize: 12, fontWeight: ‘600’ }, // titre d’un item de liste caption: { fontSize: 12, fontWeight: ‘500’ }, // info secondaire, hints // Mauvais : nommé par apparence mediumText12: { fontSize: 12, fontWeight: ‘500’ }, // ambigu, réutilisé à tort ``` **Règle** : `caption` (Medium) ≠ `listItemTitle` (SemiBold) même si la taille est identique. Ne jamais piocher un token "par approximation". --- ### Principes transverses - Un pattern = une responsabilité claire - On privilégie la simplicité locale avant la généricité globale - Le code doit rester compréhensible 6 mois plus tard - Si un pattern devient central → il mérite une décision d’architecture dédiée ⸻ ## Notes importantes - 3 bons patterns > 30 moyens - Si un pattern évolue : - on met à jour la date - on précise le nouveau contexte - En cas de doute → le pattern n’entre pas encore ici --- ## Pattern : Click-to-load strict pour les embeds tiers (iframe/widget) ### Synthèse - **Objectif** : ne charger aucun service tiers sans action explicite de l’utilisateur (performance + consentement implicite). - **Contexte** : site/webapp avec modules de réservation, map, chat ou tout embed iframe à la demande. - **Quand l’utiliser** : dès qu’un embed tiers est chargé à la demande (pas au premier rendu). - **Quand l’éviter** : si l’embed est central à la page et doit être visible immédiatement. ### Analyse - **Avantages** : - LCP non pollué par des tiers (performance-first) - Aucun tiers ne reçoit de données utilisateur sans action volontaire (consentement implicite) - Fallback toujours disponible en cas d’erreur iframe - **Limites / vigilance** : - Le fallback (lien externe + `tel:`) doit être actionnable même si l’embed échoue ### Validation - Validé le : 21-03-2026 - Contexte technique : React / Next.js — app-template-resto ### Implémentation ```tsx const [loaded, setLoaded] = useState(false); const [errored, setErrored] = useState(false); if (errored) return Ouvrir {label}; return ( <> {!loaded && } {loaded &&