Files
_Assistant_Lead_Tech/knowledge/frontend/risques/navigation.md
2026-04-02 10:17:12 +02:00

235 lines
8.1 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-02
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