mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-04-06 21:41:42 +02:00
Refonte Structure
This commit is contained in:
18
knowledge/frontend/risques/README.md
Normal file
18
knowledge/frontend/risques/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Frontend — Risques & vigilance — Index
|
||||
|
||||
Risques frontend/mobile susceptibles de provoquer des bugs subtils, comportements inattendus, dette technique ou régressions UX.
|
||||
|
||||
Avant toute proposition frontend, identifie le fichier dont le nom et la description matchent le domaine traité, puis lis-le.
|
||||
|
||||
---
|
||||
|
||||
| Fichier | Domaine | Entrées clés |
|
||||
|---------|---------|--------------|
|
||||
| `auth.md` | Auth, guards de rôle, entitlements, OAuth | Auth côté client, loading infini écran gated, bouton OAuth vide, guard rôle flash UX |
|
||||
| `state.md` | Zustand, state management, erreurs async, optimistic UI | Erreurs silencieuses, catch sans feedback, auto-reset état dégradé, fire-and-forget refresh, boolean UI hardcodé, flag isLoading unique, erreur sans rethrow, optimistic update sous-listes |
|
||||
| `navigation.md` | Expo Router, deep link, useEffect fetch, contexte store | Store vide deep link/reload, guard incomplet états terminaux, collection sans clé contexte |
|
||||
| `design-tokens.md` | Design tokens, spacing, Tailwind, StyleSheet RN | Double système espacement, dimensions via spacing, inline styles dashboard, classes Tailwind invalides |
|
||||
| `nextjs.md` | Next.js App Router, SSR, Server Actions, sécurité | useSearchParams sans Suspense, type ViewData dupliqué, composant React .ts, double validation segment, consent state ambigu, script inline XSS, window.location.reload, useTransition snapshot, window.confirm, import type server, img natif, useTransition global liste, formulaire defaultValue sans key |
|
||||
| `tests.md` | Jest, ts-jest, tests React Native | Config node bloque .tsx, faux test négatif |
|
||||
| `performance.md` | Re-renders, memoization, useCallback | Sur-renders bundle non maîtrisé, useCallback inutile inline |
|
||||
| `general.md` | Accessibilité, regex, patterns transversaux | Accessibilité oubliée a11y, regex globale singleton lastIndex |
|
||||
114
knowledge/frontend/risques/auth.md
Normal file
114
knowledge/frontend/risques/auth.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Frontend — Risques & vigilance : Auth
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/frontend/risques/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-auth-cote-client"></a>
|
||||
## Auth côté client (mauvaise séparation des responsabilités)
|
||||
|
||||
### Risques
|
||||
|
||||
- Le front "décide" des permissions au lieu d'appliquer un contrat backend
|
||||
- Affichage d'actions interdites / fuite d'informations dans l'UI
|
||||
- Tokens stockés de façon dangereuse (XSS)
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Différences entre "ce que l'UI permet" et "ce que l'API accepte"
|
||||
- Bugs "ça marche chez moi" selon sessions/rôles
|
||||
- Incohérences sur refresh / multi-tab
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Le backend reste source de vérité (authz)
|
||||
- Cacher l'UI ≠ sécuriser : toujours sécuriser côté API
|
||||
- Stockage tokens : privilégier cookies httpOnly si modèle adapté
|
||||
- Gérer proprement expiration/refresh + révocation
|
||||
|
||||
### Contexte technique
|
||||
|
||||
- Observé : (à compléter)
|
||||
- Stack : (à préciser)
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-loading-infini-ecran-gated"></a>
|
||||
## Loading infini sur écran gated par droits distants
|
||||
|
||||
### Risques
|
||||
|
||||
- Un écran protégé reste bloqué dans un faux `loading` après une erreur de chargement des entitlements
|
||||
- Un effet relance automatiquement la récupération en boucle sans action utilisateur
|
||||
- L'utilisateur ne voit ni état d'erreur ni issue de sortie claire
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Spinner infini sur un écran soumis à permissions distantes
|
||||
- `entitlements` ou autorisations laissés à `null` après erreur
|
||||
- `useEffect` ou logique d'entrée qui retrigger le fetch à chaque rendu
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Distinguer explicitement `loading`, `error`, `ready`
|
||||
- Ne pas réutiliser `null` comme état ambigu "pas encore chargé" et "chargement en erreur"
|
||||
- Bloquer les retries automatiques en boucle après erreur
|
||||
- Réautoriser un retry seulement via action utilisateur explicite ou nouvelle condition d'entrée
|
||||
- Contexte technique : React Native / Expo / store d'entitlements — 10-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-oauth-handler-vide"></a>
|
||||
## Bouton OAuth présent mais handler vide après refacto UI
|
||||
|
||||
### Risques
|
||||
|
||||
- L'OAuth est silencieusement cassé sur le nouvel écran — zéro erreur au démarrage, zéro crash
|
||||
- L'AC "toutes les fonctionnalités préservées" peut être coché alors que le bouton est mort
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `<Button title="Google" onPress={() => {}} />` — handler vide après copie depuis un ancien écran
|
||||
- OAuth fonctionnel sur l'écran précédent (`welcome.tsx`) mais absent sur le nouvel écran refactorisé
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Toute refacto UI qui introduit un bouton OAuth doit brancher le hook existant (`useGoogleAuth(onSuccess)`)
|
||||
- Si la story exclut explicitement la fonctionnalité : soit le bouton n'apparaît pas, soit `disabled` avec un label explicite ("bientôt disponible")
|
||||
- **Checklist review** : chercher `onPress={() => {}}` sur tous les boutons OAuth dans les écrans refactorisés
|
||||
- Contexte technique : Expo Router / React Native — app-alexandrie story 0.3, 19-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-guard-role-return-conditionnel"></a>
|
||||
## Guard de rôle via return conditionnel dans le render (flash UX)
|
||||
|
||||
### Risques
|
||||
|
||||
- `if (user?.role !== 'ADMIN') return <AccessDenied />` directement dans le corps du composant : pendant le chargement du store auth, `user` est `null`, ce qui déclenche un affichage momentané de l'écran "Accès refusé" avant le re-render
|
||||
- UX instable : flash visible, potentiellement suivi d'une boucle de re-render
|
||||
|
||||
### Symptômes
|
||||
|
||||
- L'écran "Accès refusé" clignote brièvement au montage avant d'afficher le bon contenu
|
||||
- Bug reproductible uniquement au chargement initial ou après un reload
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```typescript
|
||||
// ❌ Anti-pattern — flash si user === null au montage
|
||||
if (user?.role !== 'ADMIN') return <AccessDenied />;
|
||||
|
||||
// ✅ Pattern correct — useEffect + rendu vide pendant chargement
|
||||
useEffect(() => {
|
||||
if (user !== null && user.role !== 'ADMIN') {
|
||||
router.replace('/(tabs)');
|
||||
}
|
||||
}, [user, router]);
|
||||
|
||||
if (user === null || user.role !== 'ADMIN') return <View />;
|
||||
```
|
||||
|
||||
- **Règle** : tout guard de rôle dans un composant React Native doit utiliser `useEffect` + redirect + rendu vide, jamais un return conditionnel direct
|
||||
|
||||
- Contexte technique : React Native / Expo Router / Zustand auth — app-alexandrie 24-03-2026
|
||||
100
knowledge/frontend/risques/design-tokens.md
Normal file
100
knowledge/frontend/risques/design-tokens.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# Frontend — Risques & vigilance : Design Tokens
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/frontend/risques/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-double-systeme-espacement"></a>
|
||||
## Double système d'espacement dans un monorepo Expo
|
||||
|
||||
### Risques
|
||||
|
||||
- Deux échelles d'espacement coexistent avec des noms différents pour des valeurs identiques (`Spacing.three = 16` vs `spacing.base = 16`)
|
||||
- L'audit "zéro hardcode" ne détecte pas l'inconsistance car les deux sont des constantes nommées
|
||||
- Les deux échelles peuvent diverger silencieusement
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `import { Spacing } from '@/constants/theme'` coexiste avec `import { spacing } from '@/theme'`
|
||||
- Certains screens refactorisés utilisent l'ancien système sans que personne ne le détecte
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Dès la création de `src/theme/spacing.ts`, supprimer ou vider `constants/theme.ts` (sauf constantes vraiment spécifiques : `MaxContentWidth`, `BottomTabInset`)
|
||||
- Faire un `grep from '@/constants/theme'` à chaque story pour détecter les usages résiduels
|
||||
- **Cause racine** : le template Expo génère `constants/theme.ts` avec `Spacing = { one, two, three... }` — à purger explicitement lors de la story design tokens
|
||||
- Contexte technique : Expo / React Native — app-alexandrie story 0.5, 19-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-dimensions-image-via-spacing"></a>
|
||||
## Dimensions d'image via tokens `spacing` (React Native)
|
||||
|
||||
### Risques
|
||||
|
||||
- Si `spacing.huge` change pour une raison d'espacement, la taille de l'image change silencieusement
|
||||
- Régression visuelle sans que personne ne réalise l'impact — les deux changements semblent indépendants
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `width: spacing.huge, height: spacing.huge` pour une image dont la taille est fixée par la spec Figma
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```typescript
|
||||
// Correct : constante locale ou token dédié
|
||||
const THUMBNAIL_SIZE = 48; // Figma spec node 1-16147
|
||||
|
||||
// OU token dans un fichier sizes.ts dédié si la valeur est partagée
|
||||
export const sizes = { thumbnail: 48, avatar: 40 } as const;
|
||||
```
|
||||
|
||||
**Règle** : `spacing` = espacement entre éléments. `sizes` ou constantes locales = dimensions de composants.
|
||||
|
||||
- Contexte technique : React Native / design tokens — app-alexandrie story 0.4, 19-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-inline-styles-dashboard"></a>
|
||||
## Inline styles dans les composants dashboard
|
||||
|
||||
### Risques
|
||||
|
||||
- Contourne le système Tailwind + tokens CSS
|
||||
- Crée des incohérences visuelles non détectées par le linter
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `style={{ color: '#123456', marginTop: 8 }}` dans un composant dashboard
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Bloquer en code review systématiquement tout `style={{...}}` dans les composants dashboard
|
||||
- Exception acceptable uniquement : animations CSS dynamiques (valeurs calculées au runtime)
|
||||
|
||||
- Contexte technique : React / Tailwind — app-template-resto 22-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-tailwind-classes-invalides"></a>
|
||||
## Classes Tailwind invalides courantes (bugs silencieux)
|
||||
|
||||
### Risques
|
||||
|
||||
- Classes Tailwind invalides sont silencieusement ignorées — aucun warning, comportement visuellement cassé
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Item masqué affiché à pleine opacité (`opacity-55` → invalide)
|
||||
- Largeur incorrecte (`w-35` → invalide)
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
Erreurs courantes :
|
||||
- `opacity-55` → invalide. Scale : 0/5/10/20/25/30/40/50/60/70/75/80/90/95/100 → utiliser `opacity-50` ou `opacity-60`
|
||||
- `w-35` → invalide. Scale saute de `w-32` à `w-36` → utiliser `w-36`
|
||||
- `box-border` → redondant. Tailwind Preflight applique déjà `box-sizing: border-box` globalement
|
||||
|
||||
- Toujours vérifier les classes custom/non-standard avec l'extension Tailwind IntelliSense
|
||||
|
||||
- Contexte technique : Tailwind CSS — app-template-resto 22-03-2026
|
||||
63
knowledge/frontend/risques/general.md
Normal file
63
knowledge/frontend/risques/general.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Frontend — Risques & vigilance : Général
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/frontend/risques/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-accessibilite-oubliee"></a>
|
||||
## Accessibilité oubliée (a11y)
|
||||
|
||||
### Risques
|
||||
|
||||
- App inutilisable au clavier/lecteur d'écran
|
||||
- Régressions silencieuses sur focus/labels
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Modales impossibles à fermer au clavier
|
||||
- Inputs sans labels/erreurs non annoncées
|
||||
- Focus "perdu"
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Checklist a11y minimale sur chaque écran clé
|
||||
- Gestion de focus (modales, erreurs formulaire)
|
||||
- Labels/aria cohérents + tests simples
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-regex-globale-singleton-lastindex"></a>
|
||||
## Regex globale `/g` en singleton — bug `lastIndex` stateful
|
||||
|
||||
### Risques
|
||||
|
||||
- Une regex avec flag `/g` ou `/y` définie comme constante au niveau module maintient un état `lastIndex` entre les appels
|
||||
- `String.prototype.replace()` réinitialise `lastIndex`, mais `.test()` ou `.exec()` ne le font pas → bug stateful difficile à détecter, souvent introduit par un refactor ultérieur
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `.test(str)` retourne alternativement `true` / `false` sur la même chaîne selon l'ordre d'appel
|
||||
- Bug non reproductible en isolation, uniquement en séquence d'appels
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```typescript
|
||||
// ❌ RISQUÉ — regex globale partagée entre tous les appels
|
||||
const LINK_PATTERN = /https?:\/\/\S+/gi;
|
||||
function processLinks(content: string) {
|
||||
return content.replace(LINK_PATTERN, ...); // OK today
|
||||
// Mais si quelqu'un ajoute LINK_PATTERN.test(x) ailleurs → bug lastIndex
|
||||
}
|
||||
|
||||
// ✅ SÛR — nouvelle instance à chaque appel, aucun état partagé
|
||||
function makeLinkPattern(): RegExp {
|
||||
return /https?:\/\/\S+/gi;
|
||||
}
|
||||
function processLinks(content: string) {
|
||||
return content.replace(makeLinkPattern(), ...);
|
||||
}
|
||||
```
|
||||
|
||||
- **Règle** : les regex avec flag `/g` ou `/y` utilisées pour transformation de strings → toujours créer via une factory, jamais en singleton de module
|
||||
|
||||
- Contexte technique : TypeScript / React Native — app-alexandrie 24-03-2026
|
||||
83
knowledge/frontend/risques/navigation.md
Normal file
83
knowledge/frontend/risques/navigation.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Frontend — Risques & vigilance : Navigation
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/frontend/risques/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-store-vide-deep-link"></a>
|
||||
## Écran détail Expo Router — store vide en deep link / reload
|
||||
|
||||
### Risques
|
||||
|
||||
- L'écran détail (`[slug].tsx`) lit ses données depuis un store Zustand peuplé par l'écran liste
|
||||
- En deep link, kill + reopen ou navigation OS back, le store est vide → "introuvable" affiché à tort
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Écran détail vide ou erreur "non trouvé" sur accès direct (pas via la liste)
|
||||
- Fonctionne normalement en navigation standard mais échoue sur reload
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```typescript
|
||||
// useEffect de secours dans l'écran détail
|
||||
useEffect(() => {
|
||||
if (!accessToken) return;
|
||||
if (items.length > 0 || isLoading || errorState) return;
|
||||
void fetchItems(accessToken);
|
||||
}, [accessToken, items.length, isLoading, errorState, fetchItems]);
|
||||
```
|
||||
|
||||
- Ne pas afficher "introuvable" avant d'avoir vérifié que le store a bien été peuplé
|
||||
- Contexte technique : Expo Router / Zustand — app-alexandrie story 4.1, 20-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-useeffect-guard-incomplet"></a>
|
||||
## `useEffect` fetch — guard incomplet sur les états terminaux
|
||||
|
||||
### Risques
|
||||
|
||||
- Si l'état "zéro résultat intentionnel" (ex : `paywallRequired`) n'est pas dans les conditions de court-circuit, le fetch est re-déclenché à chaque re-render ou focus
|
||||
- Boucle de fetch infini sur un état métier normal
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `forums.length === 0` et `isLoading === false` → le guard ne court-circuite pas → fetch re-déclenché en boucle
|
||||
- Visible en focus sur l'écran depuis un autre onglet
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```typescript
|
||||
// ❌ Pattern à risque — re-fetch si paywallRequired (forums vide + isLoading false)
|
||||
if (forums.length > 0 || isLoading) return;
|
||||
|
||||
// ✅ Pattern correct — court-circuit sur l'état terminal
|
||||
if (forums.length > 0 || isLoading || paywallRequired) return;
|
||||
```
|
||||
|
||||
**Règle** : les états "zéro résultat intentionnel" (liste vide + flag métier) doivent être traités comme "données présentes" dans le guard de fetch.
|
||||
|
||||
- Contexte technique : React Native / Zustand / Expo Router — app-alexandrie story 4.1, 20-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-zustand-collection-sans-cle-contexte"></a>
|
||||
## Store Zustand : collections sans clé de contexte (navigation inter-contexte)
|
||||
|
||||
### Risques
|
||||
|
||||
- Un store qui stocke des collections dépendant d'un paramètre de navigation (forumSlug, threadId...) sans stocker ce paramètre affiche des données périmées lors d'une navigation inter-contexte
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Naviguer du forum A vers le forum B affiche encore les catégories/threads du forum A
|
||||
- Guard `if (items.length > 0) return` empêche le rechargement lors d'un changement de contexte
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Stocker la clé de contexte avec les données : `categoriesForumSlug: string | null`
|
||||
- Invalider si `categoriesForumSlug !== currentForumSlug` avant de retourner depuis le cache
|
||||
- Ou supprimer le guard et dépendre uniquement du changement de paramètre dans le `useEffect`
|
||||
|
||||
- Contexte technique : React Native / Zustand / Expo Router — app-alexandrie 23-03-2026
|
||||
378
knowledge/frontend/risques/nextjs.md
Normal file
378
knowledge/frontend/risques/nextjs.md
Normal file
@@ -0,0 +1,378 @@
|
||||
# Frontend — Risques & vigilance : Next.js
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/frontend/risques/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-usesearchparams-sans-suspense"></a>
|
||||
## `useSearchParams()` sans `Suspense` casse le build Next.js App Router
|
||||
|
||||
### Risques
|
||||
|
||||
- Un composant client utilisant `useSearchParams()` peut provoquer un échec de prerender/build s'il est rendu sans boundary `Suspense` depuis la page/layout serveur
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `Error: useSearchParams() should be wrapped in a suspense boundary` au `next build`
|
||||
- Fonctionne en dev mais échoue à la CI/CD
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Isoler le composant client qui utilise `useSearchParams()` et le rendre sous `<Suspense fallback={...}>` au niveau de la page
|
||||
- Ne jamais appeler `useSearchParams()` directement dans un composant rendu sans `Suspense` depuis un Server Component
|
||||
|
||||
- Contexte technique : Next.js App Router récent / Turbopack — app-template-resto 16-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-type-viewdata-duplique"></a>
|
||||
## Type `ViewData` dupliqué entre couche serveur et composant UI (Next.js)
|
||||
|
||||
### Risques
|
||||
|
||||
- TypeScript accepte deux structures identiques par structural typing — si le type source évolue, la couche UI reste désynchronisée sans erreur de compilation tant que les formes correspondent
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Deux définitions du même type dans `src/server/` et `src/app/`
|
||||
- Champ ajouté côté serveur mais absent dans le composant UI sans warning
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```typescript
|
||||
// ✅ La couche UI importe et re-exporte
|
||||
export type { PublicHomeViewData } from "@/server/public/getPublicHomeData";
|
||||
|
||||
// ❌ À éviter — redéfinition locale
|
||||
export type PublicHomeViewData = { tenantName: string; ... };
|
||||
```
|
||||
|
||||
- Règle : le type appartient à la couche qui le produit. La couche UI importe uniquement.
|
||||
|
||||
- Contexte technique : Next.js App Router / TypeScript — app-template-resto 16-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-composant-react-fichier-ts"></a>
|
||||
## Composant React dans un fichier `.ts` — `React.createElement` workaround
|
||||
|
||||
### Risques
|
||||
|
||||
- Code illisible vs JSX natif
|
||||
- Fausse impression que le fichier est "sans JSX" — peut tromper les outils de linting et les reviewers
|
||||
- Empêche l'utilisation de la syntaxe JSX si on doit ajouter des enfants complexes
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `React.createElement(...)` dans un fichier `.ts`
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Tout fichier exportant une fonction retournant un `ReactElement` ou utilisant React doit avoir l'extension `.tsx`
|
||||
- Sans exception
|
||||
|
||||
- Contexte technique : TypeScript / React — app-template-resto 16-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-double-validation-segment-app-router"></a>
|
||||
## Double validation de segment dynamique App Router (layout + page)
|
||||
|
||||
### Risques
|
||||
|
||||
- Si le layout fait `notFound()` sur un segment invalide ET que la page répète la même condition, les deux deviennent désynchronisés silencieusement lors d'une modification
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Même condition de validation dans `layout.tsx` et `page.tsx` d'un même segment
|
||||
- Modification du layout n'est pas reportée dans la page (comportement divergent)
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Si le layout garde, la page consomme — une seule responsabilité par couche
|
||||
- La page doit faire confiance à son layout parent
|
||||
- **Règle** : un seul composant est responsable de la garde sur un segment dynamique
|
||||
|
||||
- Contexte technique : Next.js App Router — app-template-resto 17-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-window-location-reload-nextjs"></a>
|
||||
## Next.js App Router : `window.location.reload()` au lieu de `router.refresh()`
|
||||
|
||||
### Risques
|
||||
|
||||
- Full reload = perd l'état React, navigation complète, plus lent
|
||||
- `router.refresh()` est l'outil idoine : retrigger le fetch des Server Components sans détruire l'état client
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `window.location.reload()` après un Server Action dans un Client Component
|
||||
- Flash de rechargement visible, perte de l'état local (scroll, focus, état de formulaire)
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```tsx
|
||||
// ❌ Anti-pattern — full reload
|
||||
await createCategoryAction(formData);
|
||||
window.location.reload();
|
||||
|
||||
// ✅ Pattern correct — RSC diff, préserve l'état client
|
||||
const router = useRouter();
|
||||
await createCategoryAction(formData);
|
||||
router.refresh();
|
||||
```
|
||||
|
||||
- `router.refresh()` refetch uniquement les Server Components affectés (via `revalidatePath`) et applique un diff. L'état des Client Components est préservé.
|
||||
|
||||
- Contexte technique : Next.js App Router — app-template-resto 21-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-consent-state-false-ambigu"></a>
|
||||
## Consent state : `false` ambigu entre "pas de décision" et "refus explicite"
|
||||
|
||||
### Risques
|
||||
|
||||
- Sans champ `decided`, `analytics: false` peut signifier "première visite" ou "refus explicite" — indistinguables
|
||||
- Le banner de consentement réapparaît à chaque visite après un refus, violant l'AC de persistance du choix
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Banner qui réapparaît après rechargement malgré un refus explicite
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```typescript
|
||||
type ConsentState = {
|
||||
analytics: boolean;
|
||||
decided: boolean; // true = l'utilisateur a fait un choix (cookie présent)
|
||||
};
|
||||
|
||||
const DEFAULT: ConsentState = { analytics: false, decided: false };
|
||||
|
||||
// À la lecture du cookie :
|
||||
if (!cookieValue) return DEFAULT; // decided=false (première visite)
|
||||
return { analytics: parsed.analytics, decided: true };
|
||||
```
|
||||
|
||||
- L'état initial du banner doit être `!decided`, pas `!analytics`
|
||||
|
||||
- Contexte technique : Next.js / cookies — app-template-resto 21-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-script-inline-interpolation-directe"></a>
|
||||
## Script inline : interpolation directe au lieu de `JSON.stringify`
|
||||
|
||||
### Risques
|
||||
|
||||
- Injection XSS potentielle via une valeur de configuration interpolée directement dans un `<Script>` inline
|
||||
- La regex de validation en amont peut évoluer et laisser passer des valeurs dangereuses
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `` {`gtag('config', '${measurementId}');`} `` — interpolation directe sans échappement
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```tsx
|
||||
// ❌ Anti-pattern — interpolation directe
|
||||
{`gtag('config', '${measurementId}');`}
|
||||
|
||||
// ✅ Pattern correct — JSON.stringify garantit l'échappement
|
||||
{`gtag('config', ${JSON.stringify(measurementId)});`}
|
||||
```
|
||||
|
||||
- S'applique aussi aux `dangerouslySetInnerHTML` et aux attributs `data-*` injectés en JS
|
||||
|
||||
- Contexte technique : Next.js / `<Script>` — app-template-resto 21-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-usetransition-snapshot-apres-setstate"></a>
|
||||
## `useTransition` + optimistic update : snapshot capturé après `setState`
|
||||
|
||||
### Risques
|
||||
|
||||
- Stale closure classique : le snapshot est capturé après `setState`, donc `categories` peut déjà référencer la nouvelle liste au moment du rollback
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Rollback optimiste qui ne restaure pas l'ancienne valeur
|
||||
- Après une erreur serveur, l'état reste sur la nouvelle liste au lieu de revenir à l'état précédent
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```tsx
|
||||
// ❌ Anti-pattern — snapshot capturé après setState
|
||||
const newList = [...categories];
|
||||
setCategories(newList);
|
||||
startTransition(async () => {
|
||||
try { await action(); }
|
||||
catch { setCategories(categories); } // peut être newList
|
||||
});
|
||||
|
||||
// ✅ Pattern correct — snapshot AVANT toute mutation d'état
|
||||
const snapshot = categories; // capturer AVANT setCategories
|
||||
setCategories(newList);
|
||||
startTransition(async () => {
|
||||
try { await action(); }
|
||||
catch { setCategories(snapshot); } // rollback garanti
|
||||
});
|
||||
```
|
||||
|
||||
- **Règle** : toujours assigner le snapshot dans un `const` **avant** le premier `setState`
|
||||
|
||||
- Contexte technique : React / Next.js App Router — app-template-resto 21-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-window-confirm-react"></a>
|
||||
## `window.confirm()` dans une app React/Next.js
|
||||
|
||||
### Risques
|
||||
|
||||
- Bloque le thread principal
|
||||
- Ne fonctionne pas en SSR
|
||||
- Non stylable, UX mobile mauvaise
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `if (!confirm("Supprimer ?")) return;` dans un Client Component
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```tsx
|
||||
// ❌ Anti-pattern
|
||||
if (!confirm("Supprimer ?")) return;
|
||||
|
||||
// ✅ Pattern correct — confirmation inline via état React
|
||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||
|
||||
{deletingId === item.id && (
|
||||
<div>
|
||||
<span>Supprimer « {item.label} » ?</span>
|
||||
<button onClick={() => { setDeletingId(null); doDelete(item.id); }}>Confirmer</button>
|
||||
<button onClick={() => setDeletingId(null)}>Annuler</button>
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
- S'applique aussi à `window.alert()` et `window.prompt()`
|
||||
|
||||
- Contexte technique : React / Next.js — app-template-resto 21-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-import-type-server-composant-client"></a>
|
||||
## `import type` depuis `src/server/**` dans un composant client
|
||||
|
||||
### Risques
|
||||
|
||||
- Violation de boundary même si l'import est type-only (effacé à la compilation)
|
||||
- Ouvre la porte à des imports runtime si le code est refactoré rapidement
|
||||
- La règle ESLint `no-restricted-imports` doit couvrir les `import type` aussi
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `import type { Foo } from "@/server/..."` dans un fichier `"use client"`
|
||||
- Passe en review car le compilateur ne bloque pas les type-only imports
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Types partagés entre server et client doivent vivre dans `src/types/` ou `src/lib/`
|
||||
- Configurer `no-restricted-imports` avec `allowTypeImports: false` pour les paths serveur
|
||||
|
||||
- Contexte technique : Next.js App Router / TypeScript — app-template-resto 22-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-img-natif-nextjs"></a>
|
||||
## Next.js : `<img>` natif interdit dans les composants
|
||||
|
||||
### Risques
|
||||
|
||||
- Warning ESLint `@next/next/no-img-element` → avec `--max-warnings=0` : erreur CI
|
||||
- Pas de lazy loading, pas d'optimisation WebP, risque de layout shift (CLS)
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `<img src="..." />` dans un composant Next.js
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Toujours utiliser `<Image>` de `next/image` à la place
|
||||
- Exception acceptable : composants de test ou storybook uniquement
|
||||
|
||||
- Contexte technique : Next.js / ESLint — app-template-resto 22-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-usetransition-global-liste-items"></a>
|
||||
## `useTransition` global pour des listes d'items interactifs
|
||||
|
||||
### Risques
|
||||
|
||||
- `isPending` global désactive **tous** les boutons de tous les items pendant qu'une opération est en cours sur un seul item
|
||||
- Sur mobile : UX bloquée, impossible d'agir pendant qu'une autre opération tourne
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Clic sur "Masquer" pour l'item A → boutons des items B et C grisés
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```tsx
|
||||
// ❌ Avant — bloque tout
|
||||
const [isPending, startTransition] = useTransition();
|
||||
// render : disabled={isPending}
|
||||
|
||||
// ✅ Après — per-item
|
||||
const [pendingId, setPendingId] = useState<string | null>(null);
|
||||
|
||||
function handleToggle(id: string) {
|
||||
setPendingId(id);
|
||||
(async () => {
|
||||
try { await toggleAction(id); }
|
||||
catch (err) { handleError(err); }
|
||||
finally { setPendingId(null); }
|
||||
})();
|
||||
}
|
||||
// render : disabled={pendingId === item.id}
|
||||
```
|
||||
|
||||
**Règles :**
|
||||
- `pendingId === item.id` pour les boutons d'item (désactive uniquement l'item en cours)
|
||||
- `pendingId !== null` pour les boutons globaux (ex: "Ajouter")
|
||||
- `finally` garantit la réinitialisation même en cas d'erreur
|
||||
|
||||
- Contexte technique : React / Next.js — app-template-resto 22-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-formulaire-defaultvalue-sans-key"></a>
|
||||
## Formulaire React avec `defaultValue` sans `key` prop
|
||||
|
||||
### Risques
|
||||
|
||||
- `defaultValue`, `defaultChecked`, `defaultSelected` ne s'appliquent qu'au montage
|
||||
- Si le composant est réutilisé (même nœud DOM, nouvelle prop) sans être démonté, les valeurs ne se mettent pas à jour
|
||||
|
||||
### Symptômes
|
||||
|
||||
- L'utilisateur édite l'entité A, clique sur "Modifier" pour l'entité B → le formulaire affiche encore les données de A
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```tsx
|
||||
// Fix obligatoire : key unique basée sur l'ID de l'entité éditée
|
||||
<EntityForm
|
||||
key={formState.mode === "edit" ? formState.entity.id : `create-${formState.contextId}`}
|
||||
...
|
||||
/>
|
||||
```
|
||||
|
||||
- **Règle** : tout formulaire d'édition réutilisé pour plusieurs entités doit avoir une `key` distincte par entité
|
||||
|
||||
- Contexte technique : React / Next.js — app-template-resto 21-03-2026
|
||||
57
knowledge/frontend/risques/performance.md
Normal file
57
knowledge/frontend/risques/performance.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Frontend — Risques & vigilance : Performance
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/frontend/risques/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-performances-sur-renders"></a>
|
||||
## Performances : sur-renders + bundle non maîtrisé
|
||||
|
||||
### Risques
|
||||
|
||||
- App lente sur mobile
|
||||
- Bundle qui grossit sans contrôle
|
||||
- Chargements inutiles (images, libs)
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Input lag
|
||||
- Temps de chargement qui dérive à chaque feature
|
||||
- Requêtes réseaux inutiles
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Lazy loading routes/features
|
||||
- Mesurer (au minimum) : temps de chargement + re-renders critiques
|
||||
- Politique images (formats, tailles, lazy)
|
||||
- Audit régulier des dépendances
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-usecallback-inutile-inline"></a>
|
||||
## `useCallback` inutile quand le callback est wrappé en inline au render
|
||||
|
||||
### Risques
|
||||
|
||||
- Le handler stable est re-wrappé dans une arrow inline lors du passage en prop → nouvelle référence à chaque render → `React.memo` ne peut pas éviter le re-render
|
||||
|
||||
### Symptômes
|
||||
|
||||
```tsx
|
||||
const handleToggle = useCallback((id: string) => { ... }, []); // stable ✓
|
||||
|
||||
// Mais au render :
|
||||
<ItemCard onToggle={() => handleToggle(item.id)} />
|
||||
// ↑ nouvelle closure à chaque render → memo inutile
|
||||
```
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- `useCallback` n'a de valeur que si le callback est passé **directement** en prop, sans re-wrapping
|
||||
- Si la signature doit capturer des variables de boucle, deux options :
|
||||
1. Passer les données en props et laisser l'enfant appeler le handler avec ses propres props
|
||||
2. Accepter que `memo` ne soit pas protégé et supprimer le `useCallback` inutile
|
||||
- Ne pas laisser un `useCallback` "pour faire bien" si son effet réel est nul
|
||||
|
||||
- Contexte technique : React — app-template-resto 22-03-2026
|
||||
|
||||
280
knowledge/frontend/risques/state.md
Normal file
280
knowledge/frontend/risques/state.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# Frontend — Risques & vigilance : State
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/frontend/risques/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-erreurs-silencieuses"></a>
|
||||
## Erreurs silencieuses / écrans blancs
|
||||
|
||||
### Risques
|
||||
|
||||
- Exceptions non gérées → app inutilisable
|
||||
- États async mal gérés → UI incohérente (loading infini, vide incompris)
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Écran blanc après une action
|
||||
- Toast générique "Une erreur est survenue" sans corrélation
|
||||
- Pas de moyen de reproduire / diagnostiquer
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Pattern "états UI explicites" (loading/empty/error)
|
||||
- Boundary d'erreur UI + fallback
|
||||
- Logging minimal côté client avec requestId/traceId quand possible
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-melange-server-client-state"></a>
|
||||
## Mélange server state / client state
|
||||
|
||||
### Risques
|
||||
|
||||
- Cache pollué par des états UI (onglets, filtres)
|
||||
- UI qui reflète une donnée périmée sans le savoir
|
||||
- Re-renders et bugs de synchronisation
|
||||
|
||||
### Symptômes
|
||||
|
||||
- "Ça revient tout seul" après refresh
|
||||
- Données affichées ≠ données du backend
|
||||
- Debug très long car état implicite
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Séparer explicitement server state vs client state
|
||||
- Invalidation/reload explicite du server state
|
||||
- État UI local réinitialisable
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-api-state-local-ecran"></a>
|
||||
## Appels API gérés en state local d'écran (refactor coûteux)
|
||||
|
||||
### Risques
|
||||
|
||||
- Server state non partageable entre écrans (liste/detail, wizard, tabs) → duplication et incohérences
|
||||
- Pas de cache / invalidation standard → bugs subtils et re-fetchs inutiles
|
||||
- Refactor tardif quand l'epic s'étend (mutations, cache, offline, pagination)
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Même appel API recopié dans plusieurs écrans
|
||||
- Un écran "A" modifie une ressource mais l'écran "B" n'est jamais rafraîchi
|
||||
- Code review qui force un refactor vers un store/cache au milieu d'un epic
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Par défaut : créer un store de domaine (ex : Zustand) ou un cache de server state pour tout domaine susceptible d'être réutilisé
|
||||
- Centraliser `isLoading`/`error`/`data` et la stratégie de refresh/invalidation
|
||||
- Exception acceptable : état purement UI, local et jetable (ex : input de recherche, filtres temporaires non persistés)
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-catch-silencieux"></a>
|
||||
## Catch silencieux — erreur inconnue sans feedback utilisateur
|
||||
|
||||
### Risques
|
||||
|
||||
- Un `catch` qui ne traite que les cas connus laisse l'utilisateur face à un spinner qui disparaît sans message
|
||||
- L'état d'erreur reste implicite → impossible de diagnostiquer ou de reproduire
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Bouton spinner qui s'arrête, rien ne se passe
|
||||
- Pas de toast / message d'erreur affiché
|
||||
- Erreur "avalée" silencieusement dans les logs
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```typescript
|
||||
} catch (err: unknown) {
|
||||
const code = (err as { code?: string }).code;
|
||||
if (code === 'SUBSCRIPTION_REQUIRED') {
|
||||
setSubscriptionRequired(true);
|
||||
} else {
|
||||
setError('Une erreur est survenue. Veuillez réessayer.'); // toujours un fallback
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **Règle** : tout `catch` doit avoir une branche `else` (ou `default`) qui affiche un feedback utilisateur explicite.
|
||||
- Contexte technique : React Native / Expo — 09-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-auto-reset-etat-degrade"></a>
|
||||
## Auto-reset d'un état dégradé sur toute réponse 2xx
|
||||
|
||||
### Risques
|
||||
|
||||
- Le client sort trop tôt d'un mode dégradé alors que la cause serveur est toujours présente
|
||||
- Le bandeau ou l'état read-only clignote puis disparaît à tort
|
||||
- Les utilisateurs retentent une action d'écriture qui va encore échouer
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Un GET réussi réinitialise `isReadOnly` ou `isDegraded`
|
||||
- L'UI redevient "normale" alors que Redis ou un service critique est toujours indisponible
|
||||
- Les erreurs reviennent immédiatement à la prochaine mutation
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Ne réinitialiser l'état dégradé qu'après une requête d'écriture réussie
|
||||
- Exclure `GET` et `HEAD` de la logique de reset
|
||||
- Conserver le mode dégradé tant qu'aucune mutation n'a prouvé le retour à la normale
|
||||
- Contexte technique : React Native / Expo — 10-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-refresh-store-fire-and-forget"></a>
|
||||
## Refresh store en fire-and-forget après mutation
|
||||
|
||||
### Risques
|
||||
|
||||
- L'UI affiche un succès alors que la resynchronisation a échoué
|
||||
- État local incohérent avec l'état serveur
|
||||
- Erreurs silencieuses impossibles à diagnostiquer
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Mutation réussie puis store jamais rafraîchi
|
||||
- Spinner coupé avant que l'écran soit réellement à jour
|
||||
- Données anciennes qui persistent jusqu'au prochain reload
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- `await` explicite du refresh si l'UI dépend du résultat
|
||||
- Gestion d'erreur dédiée sur la phase de resynchronisation
|
||||
- N'utiliser le fire-and-forget que pour un effet secondaire réellement non bloquant
|
||||
- Contexte technique : React Native / Expo — 10-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-boolean-ui-hardcode-store"></a>
|
||||
## État booléen UI dérivé hardcodé au lieu d'être calculé depuis le store
|
||||
|
||||
### Risques
|
||||
|
||||
- Un état toggle (`isBookmarked`, `isLiked`, `isFollowed`) initialisé à `false` en dur ne reflète jamais l'état réel
|
||||
- Le bouton est toujours en mode "ajouter" sans jamais passer en mode "supprimer"
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `const isBookmarked = false; // état local géré ci-dessous via state`
|
||||
- Bouton bookmark/like toujours dans le même état visuel peu importe l'état réel
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```typescript
|
||||
// ❌ Anti-pattern — état hardcodé
|
||||
const isBookmarked = false;
|
||||
|
||||
// ✅ Pattern correct — dérivé du store au rendu
|
||||
const { bookmarks } = useCommunityStore();
|
||||
const isBookmarked = bookmarks.some((b) => b.thread.id === threadId);
|
||||
```
|
||||
|
||||
- Règle : si le store contient la liste (bookmarks, likes, follows), l'état booléen se dérive avec `.some()` ou `.has()`
|
||||
|
||||
- Contexte technique : React Native / Zustand — app-alexandrie story 4.4, 20-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-flag-isloading-unique-nature-differente"></a>
|
||||
## Flag `isLoading` unique pour des opérations de natures différentes
|
||||
|
||||
### Risques
|
||||
|
||||
- Un même flag (ex: `isBookmarking`) utilisé à la fois pour les mutations (add/remove) et le chargement de la liste provoque des bugs visuels — spinner manquant au premier chargement si une mutation est en cours en parallèle
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Spinner absent au premier chargement de la liste bookmarks
|
||||
- Bouton "ajouter" désactivé alors qu'aucune mutation n'est en cours
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```typescript
|
||||
// ❌ Anti-pattern — un seul flag pour tout
|
||||
isBookmarking: boolean;
|
||||
|
||||
// ✅ Pattern correct — séparation claire
|
||||
isBookmarking: boolean; // mutations add/remove
|
||||
isLoadingBookmarks: boolean; // chargement de la liste (GET)
|
||||
```
|
||||
|
||||
- Contexte technique : React Native / Zustand — app-alexandrie story 4.4, 20-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-zustand-optimistic-update-sous-listes"></a>
|
||||
## Zustand : optimistic update sur item absent de la liste principale
|
||||
|
||||
### Risques
|
||||
|
||||
- Une action admin qui cherche l'item uniquement dans `state.threads` (liste paginée principale) manque les items présents exclusivement dans `state.pinnedThreads` ou `state.showcasedThreads`
|
||||
- L'optimistic update ne se reflète pas visuellement même si l'appel API a réussi
|
||||
|
||||
### Symptômes
|
||||
|
||||
- L'item mis à jour par une action admin n'apparaît pas dans la nouvelle sous-liste après l'action
|
||||
- Bug reproductible uniquement quand l'item est épinglé / en vitrine mais pas dans la page courante du flux principal
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```typescript
|
||||
// ❌ Anti-pattern : cherche uniquement dans la liste principale paginée
|
||||
const target = state.threads.find((t) => t.id === threadId);
|
||||
// → manque les items présents uniquement dans pinnedThreads / showcasedThreads
|
||||
|
||||
// ✅ Pattern correct : fallback sur toutes les sous-listes du store
|
||||
const target =
|
||||
state.threads.find((t) => t.id === threadId) ??
|
||||
state.pinnedThreads.find((t) => t.id === threadId) ??
|
||||
state.showcasedThreads.find((t) => t.id === threadId);
|
||||
```
|
||||
|
||||
- **Règle** : toute action qui opère sur un item pouvant être présent dans plusieurs sous-listes doit chercher dans l'ensemble de ces listes
|
||||
- Règle complémentaire : ne pas mettre à jour une sous-liste (ex: `pinnedThreads`) lors d'une action qui n'y a pas de rapport (ex: mise en vitrine ne touche pas `pinnedThreads`)
|
||||
|
||||
- Contexte technique : React Native / Zustand — app-alexandrie 23-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-zustand-erreur-sans-rethrow"></a>
|
||||
## Store Zustand : méthodes `update*` qui avalent les erreurs sans rethrow
|
||||
|
||||
### Risques
|
||||
|
||||
- Une méthode store qui catch une erreur sans la relancer (`throw`) avale silencieusement les erreurs métier (ex: `UNSAFE_LINK`)
|
||||
- L'écran appelant ne reçoit jamais l'erreur → impossible d'afficher un feedback à l'utilisateur
|
||||
|
||||
### Symptômes
|
||||
|
||||
- L'action semble réussir côté UI mais la donnée n'a pas changé en base
|
||||
- Erreurs métier (ex: lien interdit) invisibles pour l'utilisateur final
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```typescript
|
||||
// ❌ MAUVAIS — l'erreur est avalée, l'écran ne sait pas que ça a échoué
|
||||
async updateThread(forumSlug, threadId, body) {
|
||||
await communityService.updateThread(accessToken, forumSlug, threadId, body);
|
||||
},
|
||||
|
||||
// ✅ BON — l'erreur est propagée pour que l'écran puisse réagir
|
||||
async updateThread(forumSlug, threadId, body) {
|
||||
try {
|
||||
await communityService.updateThread(accessToken, forumSlug, threadId, body);
|
||||
} catch (e) {
|
||||
const err = e as Error & { code?: string };
|
||||
throw err; // Le code d'erreur (ex: UNSAFE_LINK) est préservé sur l'objet
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
- **Règle** : toute méthode store qui appelle le service réseau doit soit (1) relancer l'erreur enrichie avec `throw err`, soit (2) la stocker dans le state (`set({ error: err.message })`). Jamais les deux à la fois sans rethrow si l'écran doit réagir au catch.
|
||||
|
||||
- Contexte technique : React Native / Zustand — app-alexandrie 24-03-2026
|
||||
46
knowledge/frontend/risques/tests.md
Normal file
46
knowledge/frontend/risques/tests.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Frontend — Risques & vigilance : Tests
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/frontend/risques/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-jest-rn-config-node"></a>
|
||||
## Jest React Native — config node bloque les composants `.tsx`
|
||||
|
||||
### Risques
|
||||
|
||||
- `SyntaxError: Cannot use import statement outside a module` lors de l'import d'un barrel `.ts` qui réexporte des `.tsx`
|
||||
- Impossible d'importer des composants React Native dans les tests — JSX non transformé
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Erreur de syntaxe inattendue au run des tests sur un fichier `.ts` qui importe un `.tsx`
|
||||
- Les tests de tokens passent mais tout test touchant un composant échoue
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- `transform: { '^.+\\.ts$': 'ts-jest' }` ne transforme que `.ts` — pas `.tsx`
|
||||
- **Pattern recommandé** : tester la logique pure (tokens, valeurs de style) dans `.spec.ts`, le rendu visuel dans `.spec.tsx` avec une config séparée (`@testing-library/react-native` + `babel-jest`)
|
||||
- Exporter le `StyleSheet` de chaque composant pour le tester sans JSX (voir pattern dédié dans `10_frontend_patterns_valides.md`)
|
||||
- Contexte technique : React Native / Jest / ts-jest — app-alexandrie 19-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-faux-test-negatif"></a>
|
||||
## Faux test négatif — tester le helper au lieu de tester l'exclusion
|
||||
|
||||
### Risques
|
||||
|
||||
- Un test nommé "X n'utilise pas Y" qui appelle Y en interne est un test normal mal documenté, pas un test d'exclusion
|
||||
- Donne une fausse confiance sur le comportement par défaut du helper
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Test intitulé "sans fallback, la valeur EN vide n'est pas remplacée" mais qui appelle le helper avec fallback activé
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Un vrai test négatif vérifie que X n'importe pas Y, ou que le comportement par défaut empêche l'effet indésirable
|
||||
- Pour un helper à fallback optionnel : tester explicitement le cas `fallbackToFr=false` (défaut) avec une valeur vide
|
||||
|
||||
- Contexte technique : TypeScript / Jest — app-template-resto 17-03-2026
|
||||
Reference in New Issue
Block a user