Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 415 minIntermédiaire

Navigation avec Expo Router : pile, onglets et paramètres

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • associer chaque fichier de src/app à son écran et à son adresse ;
  • construire une navigation par pile et par onglets ;
  • passer et valider des paramètres entre écrans ;
  • protéger les écrans réservés aux utilisateurs connectés ;
  • ouvrir un écran par un lien profond sans créer de faille.

4 exercices corrigés1 schéma

Expo Router est le système de navigation d'Expo : chaque fichier du dossier src/app devient un écran, avec sa propre adresse, et des fichiers _layout.tsx disent comment ces écrans s'enchaînent, en pile ou en onglets. C'est le même principe que le routage par fichiers de Next.js. Ce chapitre construit la navigation complète d'AppliFrais : l'écran de connexion, les onglets « Mes fiches » et « Profil », le détail d'une fiche et la fenêtre d'ajout d'un frais, avec la protection des écrans réservés aux utilisateurs connectés.

Les façons de naviguer sur un téléphone

Sur un téléphone, on ne navigue pas comme dans un navigateur. Quatre modèles reviennent dans presque toutes les applications :

ModèleComportementDans AppliFrais
Pile (stack)Chaque nouvel écran se pose sur le précédent ; le bouton retour le retireListe des fiches → détail d'une fiche
Onglets (tabs)Une barre en bas de l'écran ; chaque onglet garde sa propre position« Mes fiches » et « Profil »
ModaleUn écran qui monte par-dessus le reste, pour une tâche courte« Nouveau frais »
Tiroir (drawer)Un menu qui sort du bord gauchePas utilisé

Sur Android, le bouton ou le geste retour du système doit toujours faire quelque chose de logique : revenir à l'écran précédent de la pile, fermer la modale, ou quitter l'application depuis le premier écran. Expo Router le gère pour toi tant que tu utilises ses navigateurs.

Une route par fichier

Dans src/app, le chemin d'un fichier donne l'adresse de l'écran :

FichierAdresse (route)Écran
src/app/connexion.tsx/connexionConnexion
src/app/(onglets)/index.tsx/Mes fiches
src/app/(onglets)/profil.tsx/profilProfil
src/app/fiche/[id]/index.tsx/fiche/12Détail de la fiche 12
src/app/fiche/[id]/frais.tsx/fiche/12/fraisNouveau frais sur la fiche 12
src/app/_layout.tsxaucuneLe navigateur racine
src/app/(onglets)/_layout.tsxaucuneLa barre d'onglets

Trois conventions à retenir :

  • index.tsx est l'écran par défaut de son dossier, comme index.html sur un serveur web ;
  • un nom entre crochets ([id]) est un segment dynamique : il accepte n'importe quelle valeur, que l'écran lit comme un paramètre ;
  • un dossier entre parenthèses ((onglets)) est un groupe : il organise les fichiers et porte un layout, mais n'apparaît pas dans l'adresse.
src/app/_layout.tsxconnexion.tsx(onglets)/_layout.tsxindex.tsxprofil.tsxfiche/[id]/index.tsxfrais.tsxPile racine (Stack)Connexion/connexionréservé aux visiteurs déconnectésréservé aux visiteurs connectésOnglets (Tabs)Mes fiches/Profil/profilDétail d'une fiche/fiche/12Nouveau frais/fiche/12/frais
Chaque fichier de src/app devient un écran ; les _layout.tsx décident de la pile et des onglets

Le layout racine et la pile

Le fichier src/app/_layout.tsx définit le navigateur racine. Une pile (Stack) suffit pour commencer :

tsx
import { Stack } from 'expo-router';

export default function RacineLayout() {
  return (
    <Stack>
      <Stack.Screen name="(onglets)" options={{ headerShown: false }} />
      <Stack.Screen name="fiche/[id]/index" options={{ title: 'Fiche de frais' }} />
      <Stack.Screen name="fiche/[id]/frais" options={{ title: 'Nouveau frais', presentation: 'modal' }} />
      <Stack.Screen name="connexion" options={{ headerShown: false }} />
    </Stack>
  );
}

Chaque Stack.Screen désigne un fichier par son chemin, sans extension, et règle ses options : le titre de l'en-tête, son affichage, la présentation en modale. Un écran qui n'est pas déclaré existe quand même, avec les options par défaut. L'en-tête de la pile affiche automatiquement le titre et le bouton retour.

Les onglets

Le groupe (onglets) a son propre layout, src/app/(onglets)/_layout.tsx, qui affiche une barre d'onglets :

tsx
import { Tabs } from 'expo-router/js-tabs';

export default function OngletsLayout() {
  return (
    <Tabs screenOptions={{ tabBarActiveTintColor: '#1d4ed8' }}>
      <Tabs.Screen name="index" options={{ title: 'Mes fiches' }} />
      <Tabs.Screen name="profil" options={{ title: 'Profil' }} />
    </Tabs>
  );
}

Depuis le SDK 58, un navigateur à onglets n'affiche que les écrans déclarés dans son layout : un fichier oublié dans la liste n'a pas d'onglet. Pour ajouter une icône à un onglet, passe une fonction tabBarIcon dans ses options, avec une bibliothèque d'icônes comme expo-symbols ou @react-native-vector-icons/material-design-icons.

L'imbrication est le point clé : la barre d'onglets est un écran de la pile racine. Quand Lucas touche une fiche dans l'onglet « Mes fiches », le détail se pose sur la pile, par-dessus les onglets, avec un bouton retour. C'est le comportement attendu dans une application mobile : la barre d'onglets disparaît quand on entre dans un détail.

Routes dynamiques et paramètres

L'écran src/app/fiche/[id]/index.tsx lit l'identifiant de la fiche avec le hook useLocalSearchParams :

tsx
import { useLocalSearchParams } from 'expo-router';
import { Text } from 'react-native';

export default function DetailFiche() {
  const { id } = useLocalSearchParams<{ id: string }>();
  return <Text>Fiche n° {id}</Text>;
}

Pour l'adresse /fiche/12, id vaut '12'. Les paramètres d'une adresse sont toujours des chaînes : pour appeler l'API ou comparer avec un nombre, convertis avec Number(id). Et comme n'importe quelle adresse peut être ouverte, y compris par un lien externe (plus bas), un paramètre se valide avant usage : Number('abc') vaut NaN.

Les paramètres de requête marchent aussi : /fiche/12?onglet=frais donne { id: '12', onglet: 'frais' }.

Deux façons de changer d'écran. Dans le JSX, le composant Link :

tsx
import { Link } from 'expo-router';

<Link href="/profil">Mon profil</Link>

// Avec un paramètre, et un Pressable personnalisé à la place du texte
<Link href={{ pathname: '/fiche/[id]', params: { id: 12 } }} asChild>
  <Pressable>{/* … la carte de la fiche … */}</Pressable>
</Link>

asChild transmet la navigation à l'enfant au lieu de créer un texte cliquable. Dans une fonction (après un enregistrement, par exemple), l'objet router :

tsx
import { router } from 'expo-router';

router.push({ pathname: '/fiche/[id]/frais', params: { id: 12 } }); // ajoute un écran sur la pile
router.back();                                                     // revient à l'écran précédent
router.replace('/');                                               // remplace l'écran courant, sans retour possible
router.dismiss();                                                  // ferme la modale
MéthodeEffet sur la pileQuand l'utiliser
pushAjoute un écranOuvrir un détail, un formulaire
backRetire l'écran courantAprès un enregistrement réussi
replaceRemplace l'écran courantUn écran de transition qu'on ne doit pas retrouver avec retour
dismissFerme la modale (ou des écrans de la pile)Bouton « Fermer » d'une modale
dismissToRetire les écrans jusqu'à l'adresse donnéeRevenir à la liste depuis un écran empilé trois niveaux plus loin

Dans le projet créé par create-expo-app, les routes typées sont activées ("typedRoutes": true dans app.json) : Expo génère la liste de tes routes, et l'éditeur signale un href qui ne correspond à aucun fichier. Une faute de frappe dans '/fiches/[id]' est ainsi détectée avant l'exécution.

Modifier l'en-tête depuis l'écran

Le titre déclaré dans le layout est fixe. Pour afficher le mois de la fiche dans l'en-tête, l'écran pose son propre Stack.Screen :

tsx
import { Stack } from 'expo-router';

// dans le JSX de DetailFiche, une fois la fiche chargée
<Stack.Screen options={{ title: formatMois(fiche.mois) }} />

Les options définies dans l'écran complètent celles du layout. Le même mécanisme sert à ajouter un bouton dans l'en-tête (headerRight).

Protéger les écrans réservés

Lucas ne doit voir ses fiches qu'une fois connecté. Expo Router le gère avec des routes protégées : Stack.Protected entoure les écrans réservés, et sa prop guard (la garde) dit s'ils sont accessibles.

tsx
import { SplashScreen, Stack } from 'expo-router';
import { useEffect } from 'react';

import { SessionProvider, useSession } from '@/context/session';

SplashScreen.preventAutoHideAsync(); // l'écran de démarrage reste affiché pendant le chargement

export default function RacineLayout() {
  return (
    <SessionProvider>
      <Navigation />
    </SessionProvider>
  );
}

function Navigation() {
  const { etat } = useSession();

  useEffect(() => {
    if (etat !== 'chargement') SplashScreen.hide();
  }, [etat]);

  if (etat === 'chargement') return null;
  const connecte = etat === 'connecte';

  return (
    <Stack>
      <Stack.Protected guard={connecte}>
        <Stack.Screen name="(onglets)" options={{ headerShown: false }} />
        <Stack.Screen name="fiche/[id]/index" options={{ title: 'Fiche de frais' }} />
        <Stack.Screen name="fiche/[id]/frais" options={{ title: 'Nouveau frais', presentation: 'modal' }} />
      </Stack.Protected>
      <Stack.Protected guard={!connecte}>
        <Stack.Screen name="connexion" options={{ headerShown: false }} />
      </Stack.Protected>
    </Stack>
  );
}

Quand connecte passe à true, l'écran de connexion devient inaccessible et l'application affiche le premier écran autorisé, les onglets. Quand Lucas se déconnecte, l'inverse se produit : l'écran affiché devient interdit, et Expo Router redirige vers le premier écran autorisé, la connexion. Toute tentative d'ouvrir une fiche, par le bouton retour ou par un lien, aboutit à la même redirection. Aucune navigation manuelle n'est nécessaire après la connexion ou la déconnexion : il suffit de changer l'état de la session.

Le contexte SessionProvider et le hook useSession viennent d'un fichier src/context/session.tsx, écrit au chapitre 6 quand l'application se connecte à l'API. Pendant le chargement de la session (lecture du jeton, au démarrage), le layout ne rend rien et l'écran de démarrage (splash screen) reste affiché : l'utilisateur ne voit jamais l'écran de connexion clignoter avant ses fiches.

Les liens profonds

Chaque écran a une adresse, et le scheme défini dans app.json permet de l'ouvrir depuis l'extérieur : un lien applifrais://fiche/12 dans un courriel ou une notification ouvre directement le détail de la fiche 12.

json
{
  "expo": {
    "scheme": "applifrais"
  }
}

Tu peux le tester depuis un terminal pendant le développement avec npx uri-scheme open "applifrais://fiche/12" --android. Pour une application publiée, les App Links (Android) et Universal Links (iOS) font la même chose avec une vraie adresse https://, après une vérification du domaine.

Un lien profond est une entrée externe : n'importe quelle application ou page web peut en déclencher un, avec les paramètres de son choix. Valide les paramètres comme une saisie, et ne déclenche jamais une action (supprimer, valider, payer) directement depuis un lien sans confirmation de l'utilisateur (chapitre 13).

Exercice 1 sur 4 Des fichiers aux adresses Réussi À revoir

Donne l'adresse de chaque fichier, ou « aucune » : src/app/index.tsx ; src/app/(admin)/utilisateurs.tsx ; src/app/visiteur/[id]/fiches.tsx ; src/app/(onglets)/_layout.tsx ; src/app/aide/index.tsx.

Voir la réponseMasquer la réponse de l'exercice 1
FichierAdresse
src/app/index.tsx/
src/app/(admin)/utilisateurs.tsx/utilisateurs : le groupe n'apparaît pas dans l'adresse
src/app/visiteur/[id]/fiches.tsx/visiteur/7/fiches pour le visiteur 7
src/app/(onglets)/_layout.tsxaucune : un layout n'est pas un écran
src/app/aide/index.tsx/aide

Tu avais trouvé ?

Exercice 2 sur 4 push, replace ou back ? Réussi À revoir

Choisis la méthode de router et justifie.

  1. Sur l'écran « Mes fiches », Lucas touche le bouton « Aide ».
  2. Le formulaire « Nouveau frais » vient d'être enregistré ; on revient au détail de la fiche.
  3. Un écran de bienvenue s'affiche une seule fois après l'installation, puis l'application passe aux fiches ; le bouton retour ne doit pas y ramener.
Voir la réponseMasquer la réponse de l'exercice 2
  1. router.push('/aide') : on ajoute l'écran d'aide sur la pile, le bouton retour ramène aux fiches.
  2. router.back() : on retire le formulaire, le détail de la fiche réapparaît (et se recharge s'il utilise useFocusEffect, chapitre 6).
  3. router.replace('/') : l'écran de bienvenue est remplacé, il n'est plus dans la pile.

Tu avais trouvé ?

Exercice 3 sur 4 Le paramètre qui n'est pas un nombre Réussi À revoir

Un développeur écrit dans fiche/[id]/index.tsx : const { id } = useLocalSearchParams<{ id: string }>(); if (id === 12) { … }. La condition n'est jamais vraie. Pourquoi ? Corrige, et ajoute la vérification qui protège contre une adresse comme /fiche/abc.

Voir la réponseMasquer la réponse de l'exercice 3

Les paramètres d'adresse sont des chaînes : id vaut '12', et '12' === 12 est faux (TypeScript le signale d'ailleurs). Correction :

tsx
const { id } = useLocalSearchParams<{ id: string }>();
const idFiche = Number(id);
if (!Number.isInteger(idFiche) || idFiche <= 0) {
  return <Text>Fiche introuvable.</Text>;
}

Number('abc') vaut NaN, et Number.isInteger(NaN) est faux : l'écran affiche un message au lieu d'appeler l'API avec NaN.

Tu avais trouvé ?

Exercice 4 sur 4 Ajouter un onglet Réussi À revoir

On veut un troisième onglet « Statistiques », entre « Mes fiches » et « Profil », à l'adresse /statistiques. Quels fichiers crées-tu ou modifies-tu ?

Voir la réponseMasquer la réponse de l'exercice 4
  1. Créer src/app/(onglets)/statistiques.tsx, un composant exporté par défaut.
  2. Le déclarer dans src/app/(onglets)/_layout.tsx, entre les deux autres, l'ordre des Tabs.Screen donnant l'ordre des onglets :
tsx
<Tabs.Screen name="index" options={{ title: 'Mes fiches' }} />
<Tabs.Screen name="statistiques" options={{ title: 'Statistiques' }} />
<Tabs.Screen name="profil" options={{ title: 'Profil' }} />

Le fichier étant dans le groupe (onglets), son adresse est /statistiques, et il est automatiquement protégé par la garde qui entoure (onglets) dans le layout racine.

Tu avais trouvé ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : FlatList et formulaires : afficher et saisir des donnéesRevoir le sommaire du cours