Chapitre 5.1 — Expo Router : fondations
Où on en est : Drill a un design system mais un seul écran. On installe le vocabulaire de navigation : fichiers-routes, stacks, layouts, Link.
⏱️ TL;DR — Un fichier dans
app/= un écran ;_layout.tsx= le conteneur de navigation (le Stack) ;<Link href="/history">= la navigation déclarative. La différence fondamentale avec le web : naviguer empile des écrans (avec animation native et geste retour), ça ne remplace pas la page. L’état des écrans sous la pile survit.
🎯 Objectifs
- Relier fichiers, routes et écrans (et repérer les différences avec Next.js).
- Comprendre la pile (push/pop) et ses conséquences sur l’état et le cycle de vie.
- Configurer le Stack et ses headers dans _layout.tsx.
- Naviguer avec Link (et préparer router.push pour le chapitre 5.4).
Fichiers = routes, comme à la maison
app/
├── _layout.tsx → layout racine : déclare LE navigateur (Stack)
├── index.tsx → route "/" (accueil)
├── history.tsx → route "/history" (historique)
└── settings.tsx → route "/settings" (réglages)Créer app/history.tsx crée l’écran /history — zéro enregistrement manuel (les vieux
tutos React Navigation câblaient chaque écran à la main ; Expo Router, construit dessus,
fait ça pour vous).
// app/_layout.tsx
import { Stack } from 'expo-router'
import { StatusBar } from 'expo-status-bar'
import '../global.css'
export default function RootLayout() {
return (
<>
<Stack
screenOptions={{
headerStyle: { backgroundColor: '#0b1216' },
headerTintColor: '#e2e8f0',
headerShadowVisible: false,
contentStyle: { backgroundColor: '#0b1216' },
}}
/>
<StatusBar style="light" />
</>
)
}Le <Stack> rend l’écran actif et gère la pile : animation de transition native, bouton
retour dans le header, geste retour Android. screenOptions style tous les écrans ; chaque
écran peut surcharger via <Stack.Screen options={…}> dans son propre fichier :
// app/history.tsx
import { Stack } from 'expo-router'
import { Screen } from '../components/ui/Screen'
export default function HistoryScreen() {
return (
<>
<Stack.Screen options={{ title: 'Historique' }} />
<Screen>{/* la FlatList du chapitre 3.4 */}</Screen>
</>
)
}La pile — LA différence avec le web
Sur le web, naviguer remplace le document. En mobile, naviguer empile :
Trois conséquences très concrètes :
- L’état survit : l’accueil sous la pile garde son scroll, ses useState — au retour, tout est là. (Sur le web, il faudrait le restaurer.)
- Les écrans empilés restent montés : leurs
useEffectne se « démontent » pas au push. Pour réagir à la visibilité, le hook dédié :useFocusEffect(on s’en servira pour rafraîchir l’historique en y revenant). - Le retour est un geste système : bouton/geste back Android dépile automatiquement. Vous ne gérez JAMAIS le retour à la main — vous structurez la pile pour qu’il soit juste.
Link — la navigation déclarative
import { Link } from 'expo-router'
<Link href="/history" asChild>
<Pressable className="active:opacity-80">
<AppText variant="body">Voir l'historique →</AppText>
</Pressable>
</Link>hreftypé (typed routes : une route inexistante = erreur TS — le filet de sécurité qu’on active dès maintenant).asChild: Link délègue le rendu à son enfant Pressable — indispensable pour nos composants du design system (sinon Link rend son propre Text).<Link replace …>remplace au lieu d’empiler ;pushforce un nouvel empilement même si la route est identique.
💡 Pour un dev Next.js — Correspondances directes :
app/=app/,_layout.tsx=layout.tsx,index.tsx=page.tsx,<Link href>=<Link href>. Différences à encaisser : tout est client (pas de Server Components — le « serveur » n’existe pas dans une app embarquée) ; le layout déclare un navigateur (Stack/Tabs), pas juste du JSX autour ; et la navigation a une profondeur physique (la pile) avec animation et geste système.
⚠️ Piège — Chaque écran doit être l’export default de son fichier. Un export nommé → route vide et warning discret. Et les fichiers non-écrans (composants locaux, helpers) n’ont RIEN à faire dans
app/: Metro en ferait des routes. Composants →components/, logique →lib/.
✏️ Exercices
1. Créez les trois écrans (index, history, settings) avec le kit UI, reliés par des
Link depuis l’accueil. Vérifiez : geste retour Android, conservation du scroll de l’accueil.
✅ Solution
Trois fichiers export default avec <Screen> + <Stack.Screen options={{ title }}>, accueil
avec deux <Link asChild>. Test : scroller l’accueil, pousser History, revenir par le geste →
le scroll est intact (l’écran n’a jamais été démonté). C’est la pile en action.
2. Ajoutez un console.log('mount') / cleanup log('unmount') dans un useEffect de
l’accueil, naviguez vers History puis revenez. Que loggue la console, et pourquoi ?
✅ Solution
Un seul mount au lancement — aucun unmount/remount pendant l’aller-retour : l’accueil
reste monté sous la pile. Pour réagir au focus (retour visible), useFocusEffect est le bon
outil, pas useEffect.
3. Que se passe-t-il si vous placez components/QuizHeader.tsx… dans app/components/ ?
Constatez, puis corrigez.
✅ Solution
app/components/quiz-header devient une route navigable (visible dans le typage des href
et atteignable) — pollution du routeur. Correction : sortir de app/ (→ components/). Seule
exception tolérée : les fichiers préfixés de conventions du routeur (_layout, +not-found).
🧠 Quiz
1. Que déclare le _layout.tsx racine, et que rend-il ?
Réponse
Le navigateur racine (ici un Stack) qui rend l’écran actif, gère la pile, les transitions et le header — plus les éléments globaux (StatusBar, import du CSS NativeWind).
2. Trois différences entre naviguer sur le web et pousser un écran mobile ?
Réponse
Le push empile sans démonter (état conservé), la transition est animée nativement, et le retour est un geste système qui dépile — pas un lien à gérer.
3. À quoi sert asChild sur Link ?
Réponse
À déléguer le rendu au composant enfant (notre Pressable/Button du kit) : Link fournit le comportement de navigation, l’enfant fournit l’apparence.
4. useEffect suffit-il pour « à chaque fois que l’écran redevient visible » ?
Réponse
Non — l’écran reste monté sous la pile, l’effet ne rejoue pas. C’est le rôle de useFocusEffect (déclenché à chaque focus de l’écran).
5. Pourquoi ne met-on pas de composants dans app/ ?
Réponse
Tout fichier de app/ devient une route. Les composants vivent dans components/, la
logique dans lib/ — app/ ne contient que des écrans et les fichiers de convention du routeur.
👉 Chapitre suivant : 5.2 — Tabs — les 4 onglets de Drill.