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>
15 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-07 source_projects: [app-alexandrie, RL799_V2]
Frontend — Risques & vigilance : Navigation
Extrait de la base de connaissance Lead_tech. Voir
knowledge/frontend/risques/README.mdpour l'index complet.
É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
// 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 === 0etisLoading === 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) returnempê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 !== currentForumSlugavant 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 versapp/(tabs)/x.tsx— letitle(label affiché) est totalement indépendant du routage- Un label "Communauté" sur
name="explore"afficheexplore.tsxsans 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
nameet 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
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 tableauroutescré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 enfantpath: '', soit via un guard globalbeforeEach, mais pas via une seconde route racine - Contexte technique : Vue 3 / Vue Router 4 — RL799_V2, 02-04-2026
Navigation disabled via router-link + blocage au click
Risques
- Un
router-linkrendu 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.preventbloque 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
É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.iddans unref()au montage sanswatch - 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 (watchsur 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
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 dubeforeEach - Un changement dans le guard redirige mal (
loginvshome) 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 (
trueou{ 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
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
Fichiers non-route sous src/app avec Expo Router
Risques
- Expo Router scanne récursivement
src/apppour 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 existouProperty 'jest' doesn't existdans Expo Go (le runtime de l'app charge un fichier de test)
Bonnes pratiques / mitigations
-
src/appne 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/jestau runtime, vérifier immédiatement qu'aucun fichier de test n'est resté soussrc/app -
Contexte technique : Expo Router — app-alexandrie 25-06-2026
Découplage icône / destination lors d'un renommage de route
Risques
- Lors du renommage d'une route (ex.
/settings→/profile), l'URL, l'accessibilityLabelet le texte visible sont mis à jour, mais l'icône est oubliée - Le grep capte les
href=/router.pushmais 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 :
- La destination (
href,router.push,<Redirect>) - L'
accessibilityLabel - Le texte visible (
label,title) - L'icône (
<Ionicons name="..." />,<MaterialIcons name="..." />)
// ❌ 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
Sous-items de nav partageant un path, distincts par la query → activation sur le path seul
Risques
- Deux liens
/x?mode=aet/x?mode=bdans une sidebar dont l'activation faitroute.path.startsWith(item.to) - Double piège : si
item.toest le path pur (/x), les deux s'allument ; siitem.tocontient déjà la query (/x?mode=a), aucun ne s'allume (route.pathvaut/xsans 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
- Garder
item.tocomme path pur pour la cible router-link, OU splitter path/query avant de passer à router-link (Vue Router encode le?si on passe une stringpath?query→ la route ne matche pas) - Passer
{ path, query }séparés à router-link, pas une string concaténée - Ajouter un discriminant explicite
queryMatch: { key, value }et testerroute.query[key] === valuepour l'activation
- Contexte technique : Vue 3 / Vue Router — RL799 (sidebar admin Surveillants, query
?office=), revue adversariale de spec, 14-06-2026