mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-06-28 01:53:40 +02:00
5f5c87296e
Triage et intégration des propositions frontend du buffer 95_a_capitaliser.md (lot local RL799_V2/Vue3 + app-alexandrie/RN-Expo, mai-juin 2026). ~73 entrées intégrées sur knowledge/frontend/ + 1 nouveau fichier, dont : - patterns/state.md : race-token partagé latest-wins (fusion 3 props), capture sync anti-race, event bus timestamp, clé cache composite, état dérivé = computed - risques/state.md : 9 risques Zustand/store (fetchId reset, useRef remount, re-fetch infini sur [], flag optimiste écrasé, cache détail/liste stale, latch sans reset, :key index) - patterns/navigation.md : Expo Router (tab bar, useFocusEffect, Href typé, routing pur fusionné) - patterns/general.md : helpers temps purs, composants génériques + skeleton, fail-fast, touch target - risques/general.md : 24 risques (sweep statique, filtre client liste paginée, hooks avant return, a11y VoiceOver/disabled, redirection allowlist, RangeError toISOString, section i18n...) - design-tokens (cluster theming light/dark MD3), tests, performance, react-native, nextjs - NOUVEAU risques/responsive.md (gating par capacité d'input + checklist régressions mobile) - READMEs patterns/risques mis à jour Doublons inter-fichiers évités (vérifié : aucune ancre dupliquée introduite). Rejets (doublons 91/9/87), reciblages workflow (156/257) et bloc 32 (CLAUDE projet) non intégrés ici. Source 95_a_capitaliser.md non purgée (purge en fin de capitalisation complète). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
382 lines
15 KiB
Markdown
382 lines
15 KiB
Markdown
---
|
|
title: Frontend — Risques & vigilance : Navigation
|
|
domain: frontend
|
|
bucket: risques
|
|
tags: [navigation, expo-router, vue-router, vue, zustand, useeffect, deep-link, a11y]
|
|
applies_to: [implementation, review, debug]
|
|
severity: high
|
|
validated_on: 2026-04-07
|
|
source_projects: [app-alexandrie, RL799_V2]
|
|
---
|
|
|
|
# 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
|
|
|
|
---
|
|
|
|
<a id="risque-tabs-name-label-inverses"></a>
|
|
## Expo Router — mapping `name`/`label` des tabs inversés sans erreur
|
|
|
|
### Risques
|
|
|
|
- `<Tabs.Screen name="x">` route vers `app/(tabs)/x.tsx` — le `title` (label affiché) est totalement indépendant du routage
|
|
- Un label "Communauté" sur `name="explore"` affiche `explore.tsx` sans aucune erreur de build ni de lint
|
|
|
|
### Symptômes
|
|
|
|
- Le bon label est affiché, mais l'écran affiché est celui d'un boilerplate ou d'un autre module
|
|
- Bug invisible jusqu'au test manuel de chaque onglet
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
- Lors de tout ajout ou renommage de tab, valider visuellement que chaque label correspond à l'écran attendu
|
|
- Convention : aligner le `name` et le nom de fichier avec le wording du label (ex : `name="community"` → `community.tsx` → `title="Communauté"`)
|
|
- Ajouter un test de smoke de navigation si la structure de tabs est critique
|
|
|
|
- Contexte technique : Expo Router — app-alexandrie, 25-03-2026
|
|
|
|
---
|
|
|
|
<a id="risque-cross-groupe-router-push"></a>
|
|
## Expo Router — ne jamais préfixer le groupe dans `router.push`
|
|
|
|
### Risques
|
|
|
|
- `router.push('/(auth)/forgot-password')` depuis un écran `(tabs)/` peut échouer silencieusement ou lever une erreur selon la version d'Expo Router
|
|
- La résolution des groupes de routes se fait par contexte de navigation — un préfixe de groupe explicite n'est pas un chemin de route valide
|
|
|
|
### Symptômes
|
|
|
|
- Navigation vers un écran `(auth)/` qui n'aboutit pas ou lève une erreur au runtime
|
|
- Fonctionne dans certaines versions d'Expo Router mais pas d'autres
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
```typescript
|
|
// ❌ Anti-pattern — préfixe de groupe explicite
|
|
router.push('/(auth)/forgot-password');
|
|
|
|
// ✅ Pattern correct — chemin sans groupe
|
|
router.push('/forgot-password' as never);
|
|
```
|
|
|
|
- Règle : les groupes `(auth)`, `(tabs)`, etc. sont des conventions d'organisation de fichiers, pas des segments de route — ne jamais les inclure dans les appels de navigation programmatique
|
|
|
|
- Contexte technique : Expo Router — app-alexandrie, 25-03-2026
|
|
|
|
---
|
|
|
|
<a id="risque-vue-router-double-route-racine"></a>
|
|
## Vue Router 4 — double route `/` avec `redirect` + layout parent
|
|
|
|
### Risques
|
|
|
|
- Déclarer à la fois `{ path: '/', redirect: ... }` et `{ path: '/', component: Layout, children: [...] }` dans le même tableau `routes` crée un conflit silencieux
|
|
- La première route `/` capturée empêche l'accès normal aux enfants du layout sur les accès directs à `/`
|
|
|
|
### Symptômes
|
|
|
|
- La redirection de `/` semble "fonctionner" en dev, mais uniquement parce qu'un guard global compense le problème
|
|
- Les enfants du layout parent ne sont jamais atteints directement depuis `/`
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
```typescript
|
|
// ❌ Anti-pattern — deux routes racine concurrentes
|
|
const routes = [
|
|
{ path: '/', redirect: '/home' },
|
|
{ path: '/', component: Layout, children: [...] },
|
|
];
|
|
|
|
// ✅ Pattern correct — une seule route racine + redirect enfant
|
|
const routes = [
|
|
{
|
|
path: '/',
|
|
component: Layout,
|
|
children: [
|
|
{ path: '', redirect: '/home' },
|
|
],
|
|
},
|
|
];
|
|
```
|
|
|
|
- Règle : ne jamais faire coexister deux entrées `path: '/'` concurrentes dans Vue Router
|
|
- Gérer la redirection de `/` soit via un enfant `path: ''`, soit via un guard global `beforeEach`, mais pas via une seconde route racine
|
|
- Contexte technique : Vue 3 / Vue Router 4 — RL799_V2, 02-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-router-link-disabled"></a>
|
|
## Navigation disabled via `router-link` + blocage au click
|
|
|
|
### Risques
|
|
|
|
- Un `router-link` rendu comme "disabled" continue de produire un `<a href="...">` valide dans le DOM
|
|
- Le lien reste accessible au clavier, aux lecteurs d'écran et aux tests a11y, même si `@click.prevent` bloque la navigation
|
|
|
|
### Symptômes
|
|
|
|
- Élément affiché avec `aria-disabled="true"` mais encore focusable et tabulable
|
|
- Faux positif visuel : l'UI semble désactivée, mais le DOM expose toujours un lien actif
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
```html
|
|
<!-- ❌ Anti-pattern -->
|
|
<router-link
|
|
:to="item.to"
|
|
aria-disabled="true"
|
|
@click.prevent="item.disabled"
|
|
>
|
|
{{ item.label }}
|
|
</router-link>
|
|
|
|
<!-- ✅ Pattern correct -->
|
|
<span
|
|
v-if="item.disabled"
|
|
class="nav__item nav__item--disabled"
|
|
aria-disabled="true"
|
|
>
|
|
{{ item.label }}
|
|
</span>
|
|
<router-link
|
|
v-else
|
|
:to="item.to"
|
|
class="nav__item"
|
|
>
|
|
{{ item.label }}
|
|
</router-link>
|
|
```
|
|
|
|
- Règle : un élément de navigation désactivé ne doit jamais être un lien
|
|
- Utiliser un élément non interactif (`span`) ou un vrai contrôle désactivable (`button disabled`) selon le besoin
|
|
- Contexte technique : Vue 3 / Vue Router 4 / accessibilité — RL799_V2, 02-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-etat-local-depuis-query-param"></a>
|
|
## État local initialisé depuis un query param de route sans synchronisation réactive
|
|
|
|
### Risques
|
|
|
|
- Un formulaire branché sur un query param de route peut soumettre un identifiant obsolète si la prop initiale est copiée une seule fois dans un état local
|
|
- Le bug est discret et passe facilement les tests textuels
|
|
|
|
### Symptômes
|
|
|
|
- Composant qui copie `route.query.id` dans un `ref()` au montage sans `watch`
|
|
- Navigation intra-page (même composant, query param différent) qui soumet l'ancien identifiant
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
- Quand un composant initialise un état local depuis une prop liée au router (ex: `route.query.*`), ajouter une synchronisation réactive explicite (`watch` sur la prop) ou utiliser directement la prop si possible
|
|
- Ajouter un test qui valide la synchro sur changement de query param (même composant réutilisé, navigation intra-page)
|
|
|
|
- Contexte technique : Vue 3 / Vue Router 4 — RL799_V2 02-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-vue-router-tests-textuels-guards"></a>
|
|
## Vue Router — faux sentiment de sécurité avec tests textuels sur guards
|
|
|
|
### Risques
|
|
|
|
- Des assertions de type `content.includes("requiresRoles: ['admin']")` valident la présence de configuration mais pas le comportement réel du `beforeEach`
|
|
- Un changement dans le guard redirige mal (`login` vs `home`) ou ignore un cas RBAC sans casser les tests
|
|
|
|
### Symptômes
|
|
|
|
- Les tests passent alors qu'un changement dans le guard redirige vers la mauvaise page
|
|
- Régressions de guard runtime invisibles aux tests
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
- Ajouter au moins un test runtime qui exécute effectivement la logique du guard (session admin/non-admin) et vérifie la valeur retournée par le guard (`true` ou `{ name: 'home' }`)
|
|
- Les tests textuels (`includes`) sont acceptables comme smoke structurel mais ne doivent pas être la seule couverture des guards de navigation
|
|
- **Signal review** : guards de navigation couverts uniquement par des tests `includes()` sans test comportemental
|
|
|
|
- Contexte technique : Vue 3 / Vue Router 4 / node:test — RL799_V2 08-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-navigation-router-push-undefined"></a>
|
|
## `router.push` construit avec segment potentiellement `undefined`
|
|
|
|
### Risques
|
|
- Navigation vers `/.../undefined`, historique pollué et diagnostics trompeurs.
|
|
|
|
### Symptômes
|
|
- Warnings Vue Router en cascade au boot ou à la reprise d'événements globaux.
|
|
|
|
### Bonnes pratiques / mitigations
|
|
- Valider les segments dynamiques avant `push/replace`.
|
|
- Préférer la navigation nommée avec params validés plutôt que concaténation de string.
|
|
|
|
- Contexte technique : Vue Router / navigation dynamique — RL799_V2 15-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-fichiers-non-route-sous-src-app-expo-router"></a>
|
|
## Fichiers non-route sous `src/app` avec Expo Router
|
|
|
|
### Risques
|
|
|
|
- Expo Router scanne récursivement `src/app` pour construire les routes : tout fichier TypeScript laissé dans cet arbre (`*.spec.ts`, `*.utils.ts`, `*.screen-logic.ts`) est traité comme une route potentielle, même s'il s'agit d'un test, d'un helper ou d'une logique d'écran
|
|
- Symptômes initiaux trompeurs : une succession de warnings au build puis un crash runtime qui ressemble à un problème d'app, alors que la cause est un simple fichier mal placé
|
|
|
|
### Symptômes
|
|
|
|
- Warning `Route "...spec.ts" is missing the required default export`
|
|
- Warning `Route "...screen-logic.ts" is missing the required default export`
|
|
- Erreur runtime `Property 'describe' doesn't exist` ou `Property 'jest' doesn't exist` dans Expo Go (le runtime de l'app charge un fichier de test)
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
- `src/app` ne doit contenir **que** des fichiers de routing Expo Router
|
|
- Déplacer helpers, utils, tests et logique d'écran ailleurs (`src/domains`, `src/features`, `src/shared`)
|
|
- **Diagnostic** : en debug mobile, si Expo Go remonte `describe` / `jest` au runtime, vérifier immédiatement qu'aucun fichier de test n'est resté sous `src/app`
|
|
|
|
- Contexte technique : Expo Router — app-alexandrie 25-06-2026
|
|
|
|
---
|
|
|
|
<a id="risque-icone-decouplee-destination-renommage-route"></a>
|
|
## Découplage icône / destination lors d'un renommage de route
|
|
|
|
### Risques
|
|
|
|
- Lors du renommage d'une route (ex. `/settings` → `/profile`), l'URL, l'`accessibilityLabel` et le texte visible sont mis à jour, mais **l'icône** est oubliée
|
|
- Le grep capte les `href=`/`router.push` mais pas les `<Icon name="..." />`, souvent rangés dans des constantes éloignées de la logique de nav
|
|
|
|
### Symptômes
|
|
|
|
- L'utilisateur voit une icône engrenage menant à un écran "Profil"
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
À chaque callsite d'une route renommée, auditer **4 dimensions**, pas seulement l'URL :
|
|
|
|
1. La destination (`href`, `router.push`, `<Redirect>`)
|
|
2. L'`accessibilityLabel`
|
|
3. Le texte visible (`label`, `title`)
|
|
4. **L'icône** (`<Ionicons name="..." />`, `<MaterialIcons name="..." />`)
|
|
|
|
```tsx
|
|
// ❌ engrenage vers un écran Profil
|
|
<Pressable onPress={() => router.push('/(tabs)/profile')} accessibilityLabel="Profil">
|
|
<Ionicons name="settings-outline" size={28} />
|
|
</Pressable>
|
|
// ✅
|
|
<Pressable onPress={() => router.push('/(tabs)/profile')} accessibilityLabel="Profil">
|
|
<Ionicons name="person-circle-outline" size={28} />
|
|
</Pressable>
|
|
```
|
|
|
|
- Penser aussi aux constantes de tabs (`TAB_CONFIG`, `TABS`) lors du grep
|
|
- Contexte technique : React Native / Expo Router — app-alexandrie review IA-v2.1, 27-05-2026
|
|
|
|
---
|
|
|
|
<a id="risque-nav-activation-path-vs-query"></a>
|
|
## Sous-items de nav partageant un path, distincts par la query → activation sur le path seul
|
|
|
|
### Risques
|
|
|
|
- Deux liens `/x?mode=a` et `/x?mode=b` dans une sidebar dont l'activation fait `route.path.startsWith(item.to)`
|
|
- Double piège : si `item.to` est le path pur (`/x`), les deux s'allument ; si `item.to` contient déjà la query (`/x?mode=a`), aucun ne s'allume (`route.path` vaut `/x` sans query)
|
|
|
|
### Symptômes
|
|
|
|
- Deux entrées de sidebar actives simultanément, ou aucune active, sur des routes qui ne diffèrent que par la query
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
1. Garder `item.to` comme **path pur** pour la cible router-link, OU splitter path/query avant de passer à router-link (Vue Router encode le `?` si on passe une string `path?query` → la route ne matche pas)
|
|
2. Passer `{ path, query }` séparés à router-link, pas une string concaténée
|
|
3. Ajouter un discriminant explicite `queryMatch: { key, value }` et tester `route.query[key] === value` pour l'activation
|
|
|
|
- Contexte technique : Vue 3 / Vue Router — RL799 (sidebar admin Surveillants, query `?office=`), revue adversariale de spec, 14-06-2026
|
|
|
|
---
|