docs(knowledge): capitalisation frontend — intégration du triage local (mai-juin 2026)

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>
This commit is contained in:
MaksTinyWorkshop
2026-06-25 15:31:53 +02:00
parent f1b783407a
commit 5f5c87296e
15 changed files with 2439 additions and 12 deletions
+215
View File
@@ -277,3 +277,218 @@ const handleSubmit = async () => {
- Détecter via query string (pollue URL, manipulable)
---
<a id="pattern-tab-bar-native-cachee-minimum-vital"></a>
## Pattern : Tab bar native cachée → ne garder que le wiring routing
### Synthèse
- **Objectif** : quand la tab bar native d'Expo Router est désactivée (`tabBarStyle.display: 'none'`) au profit d'une nav persistante custom, ne garder dans `(tabs)/_layout.tsx` que le minimum vital de déclaration de routes.
- **Contexte** : Expo Router avec `<PersistentTabBar />` custom remplaçant la tab bar native.
- **Quand l'utiliser** : dès que le visuel des tabs vit dans un composant custom et non dans `<Tabs>`.
- **Quand l'éviter** : tab bar native conservée (le `label`/icônes restent utiles).
### Analyse
- **Avantages** :
- les `<Tabs.Screen>` ne servent plus qu'à déclarer les routes physiques du dossier `(tabs)/` (sinon 404 sur certaines plateformes)
- suppression du dead-code visuel (`label`, `iconActive`, `tabBarBadge`…) et de ses dépendances inutiles (icons, stores de badges)
- l'intention "wiring routing pur, pas du visuel" devient explicite (`TAB_ROUTE_NAMES`)
- **Limites / vigilance** :
- un `TAB_CONFIG` riche "au cas où on réactiverait la tab bar native" masque l'intention et crée des incohérences silencieuses (ordre du config ≠ ordre visuel réel)
- **règle complémentaire** : tout hook/effet de boot alimentant un état consommé par la nav persistante (ex. `refreshUnreadCount` pour la cloche TopBar) appartient au `_layout.tsx` **racine**, pas au layout `(tabs)/` — sinon il ne s'exécute que sur les routes `(tabs)/*` et le badge ne s'hydrate pas ailleurs
### Validation
- Validé le : 27-05-2026
- Contexte technique : React Native / Expo Router — app-alexandrie (IA-v2.1)
### Implémentation
```tsx
// Les <Tabs.Screen> déclarent uniquement les routes physiques.
// La nav visuelle vit dans <PersistentTabBar />.
const TAB_ROUTE_NAMES = ['explore', 'community', 'library', 'profile',
'notifications', 'achievements', 'settings'] as const;
return (
<Tabs screenOptions={{ headerShown: false, tabBarStyle: { display: 'none' } }}>
{TAB_ROUTE_NAMES.map((name) => <Tabs.Screen key={name} name={name} />)}
</Tabs>
);
```
---
<a id="pattern-etat-ui-ephemere-usefocuseffect"></a>
## Pattern : État UI éphémère lié au focus écran via `useFocusEffect` (Expo Router)
### Synthèse
- **Objectif** : nettoyer un état UI éphémère qu'un écran pose dans un composant global persistant (TopBar, BottomBar, FAB contextuel) au **blur**, pas au démontage.
- **Contexte** : Expo Router, où un écran enfant n'est pas toujours démonté à la navigation arrière (gardé en arrière-plan, surtout en web).
- **Quand l'utiliser** : dès qu'un écran pose un état consommé par un composant global persistant.
- **Quand l'éviter** : état strictement local à l'écran, sans surface globale.
### Analyse
- **Avantages** : le cleanup se déclenche à la perte de focus, garanti même si l'écran reste monté.
- **Limites / vigilance** :
- `useEffect` + cleanup au démontage laisse un titre/état fantôme sur le parent (ex. titre "Bob" qui reste après retour sur la liste Messages)
- bug souvent invisible sur natif (démontage plus systématique), visible en web → smoke multi-plateforme indispensable
### Validation
- Validé le : 27-05-2026
- Contexte technique : React Native / Expo Router — app-alexandrie (IA-v2.2, `useTopBarStore`)
### Implémentation
```ts
// ❌ titre fantôme : cleanup au unmount, pas garanti au blur
useEffect(() => {
if (!peerHandle) return;
useTopBarStore.getState().setTitle(peerHandle);
return () => useTopBarStore.getState().setTitle(null);
}, [peerHandle]);
// ✅ cleanup au blur
import { useFocusEffect } from 'expo-router';
useFocusEffect(useCallback(() => {
useTopBarStore.getState().setTitle(peerHandle);
return () => useTopBarStore.getState().setTitle(null);
}, [peerHandle]));
```
---
<a id="pattern-routes-expo-router-typees-href"></a>
## Pattern : Routes Expo Router typées en `Href` au lieu de `as never`
### Synthèse
- **Objectif** : supprimer le cast `router.push('/route' as never)` répété à chaque callsite au profit de constantes `Href` centralisées.
- **Contexte** : Expo Router sans typed routes générés, qui pousse à caster en `never`.
- **Quand l'utiliser** : dès qu'une route est poussée depuis plusieurs callsites.
- **Quand l'éviter** : typed routes activés (le type généré suffit).
### Analyse
- **Avantages** :
- 1 seul cast par route au lieu d'un par callsite
- migration future vers `typedRoutes` triviale (remplacer `as Href` par le type généré sans toucher aux callsites)
- auto-complétion sur les constantes plutôt que sur des string literals
- **Limites / vigilance** : ne pas confondre avec la navigation réactive — les routes restent poussées dans des handlers, pas dans un `useEffect` (voir `risques/navigation.md`).
### Validation
- Validé le : 27-05-2026
- Contexte technique : React Native / Expo Router — app-alexandrie (IA-v2.5 L4)
### Implémentation
```typescript
import { useRouter, type Href } from 'expo-router';
const CHECKOUT_HREF = '/subscription/checkout' as Href;
const PACKS_HREF = '/packs' as Href;
// callsites :
onPress={() => router.push(CHECKOUT_HREF)}
onPress={() => router.push(PACKS_HREF)}
```
---
<a id="pattern-stack-independant-par-tab-derniere-route"></a>
## Pattern : Stack indépendant par tab via mémorisation de la dernière route
### Synthèse
- **Objectif** : qu'un tap sur un onglet re-atterrisse sur sa dernière route visitée (pas sur la racine) quand la `Tabs` native est désactivée + BottomBar custom.
- **Contexte** : Expo Router, `Tabs` native off, `<PersistentTabBar />` custom.
- **Quand l'utiliser** : quick win avant un refactor archi `(app)/` complet (option a).
- **Quand l'éviter** : besoin d'un back-stack complet par tab (préférer alors un `Stack` natif par tab).
### Analyse
- **Avantages** : low-risk, sans refactor archi ; mémorise la dernière route par tab dans un store.
- **Limites / vigilance** :
- **garde-fou clé** : exclure les routes auth/onboarding du tracking, sinon le tap sur un onglet ramène vers un onboarding terminé → écran vide/crash
- limites assumées en v2 : seulement la **dernière** route (pas de back-stack), pas de persistance disque (perdu au cold start)
### Validation
- Validé le : 27-05-2026
- Contexte technique : React Native / Expo Router — app-alexandrie (IA-v2.8 AC5)
### Implémentation
```typescript
type RememberableTab = 'explore' | 'community' | 'messages' | 'library';
const EXCLUDED = ['/(auth)', '/login', '/onboarding'];
export const useTabHistoryStore = create<{
lastRoutes: Partial<Record<RememberableTab, string>>;
setLastRoute: (tab: RememberableTab, route: string) => void;
}>((set) => ({
lastRoutes: {},
setLastRoute: (tab, route) => {
if (EXCLUDED.some((p) => route.startsWith(p))) return; // garde-fou
set((s) => ({ lastRoutes: { ...s.lastRoutes, [tab]: route } }));
},
}));
// _layout.tsx racine : observe pathname → pose la dernière route par tab
// BottomBar : au tap d'un tab non-actif, router.push(lastRoutes[tab] ?? tab.href)
```
---
<a id="pattern-routing-decorrele-rendu-fn-pure"></a>
## Pattern : Routing décorrélé du rendu (builder pur testable)
### Synthèse
- **Objectif** : extraire le calcul de la route cible (`(payload) => Href | null`) du composant qui navigue, pour le rendre pur et testable en env node.
- **Contexte** : navigation conditionnelle (route dépendant du payload, du type d'événement, de l'état utilisateur) ; Expo Router et autres routers.
- **Quand l'utiliser** : dès qu'une route est templatée inline ou qu'un `switch` de routing vit dans un composant.
- **Quand l'éviter** : navigation triviale vers une route fixe.
### Analyse
- **Avantages** :
- testable env node : 0 mock RN, 0 `NavigationContainer`, juste `expect(fn(...)).toBe('/path/x')`
- cas edge centralisés : un seul `switch`, un seul fallback `null`, un seul format d'URL
- type-safe si typed routes activés (le `pathname` est vérifié contre l'arbre réel)
- **pas de slug hardcodé** : si la source ne connaît pas le slug, on omet le param plutôt que de mentir — l'écran cible retombe sur sa résolution serveur
- **Limites / vigilance** :
- anti-pattern : `router.push(\`/community/thread/${id}?forumSlug=general\`)` quand `'general'` n'a aucune chance d'être le bon slug → fonctionne par accident (fallback serveur), illisible
- quand le caller a l'objet complet, exposer une **2ᵉ** fonction qui prend l'objet et délègue à la primitive (évite de dupliquer le `switch`)
### Validation
- Validé le : 29-05-2026
- Contexte technique : React Native / Expo Router — app-alexandrie (ux-cleanup-4 / ux-cleanup-9 / ux-cleanup-10 ; `feed-navigation.ts`, `notifications-navigation.ts`)
### Implémentation
```ts
// builder pur — forme objet { pathname, params }
export function buildThreadRoute(item: { id: string; forum?: { slug: string } | null }) {
const slug = item.forum?.slug;
return {
pathname: '/community/thread/[threadId]',
params: slug ? { threadId: item.id, forumSlug: slug } : { threadId: item.id },
} as const;
}
// primitive (bouts un par un) + dérivée (objet complet) coexistent
export function resolveNotificationRoute(type, targetId, commentId): string | null { /* switch */ }
export function routeForNotification(n: NotificationItem): string | null {
return resolveNotificationRoute(n.targetType, n.targetId, n.commentId);
}
// composant : délègue, ne décide pas
const route = resolveNotificationRoute(notif.targetType, notif.targetId, notif.commentId);
if (route) router.push(route);
```
---