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

8.1 KiB

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.


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

// 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

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

// ❌ 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

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


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.tsxtitle="Communauté")

  • Ajouter un test de smoke de navigation si la structure de tabs est critique

  • Contexte technique : Expo Router — app-alexandrie, 25-03-2026


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

// ❌ 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


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

// ❌ 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

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

<!-- ❌ 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