Navigation : Expo Router
Une application, ce n'est pas un seul écran : c'est une dizaine d'écrans entre lesquels l'utilisateur circule en permanence. Comment passe-t-on de l'accueil à un exercice, puis à une séance, puis aux réglages ? Comment revient-on en arrière ? Comment empêche-t-on quelqu'un de non connecté d'atterrir sur ces écrans ? Tout cela, c'est le travail de la navigation. Dans Halterofit, elle repose sur un principe d'une élégance redoutable : la structure des fichiers EST la carte des écrans. Pas de configuration de routes à part — tu crées un fichier, tu obtiens un écran.
Qu'est-ce que « naviguer », au fond ?
Avant de parler d'Expo Router, prenons un peu de hauteur. « Naviguer » dans une application mobile, c'est gérer la question : quel écran est visible maintenant, et que se passe-t-il quand l'utilisateur touche un bouton qui mène ailleurs ? Cela paraît trivial vu de l'extérieur, mais c'est l'un des problèmes les plus structurants de toute app. Car ce n'est pas qu'une affaire d'affichage : il faut se souvenir d'où l'on vient (pour le bouton retour), conserver ou jeter l'état des écrans précédents, animer les transitions, et décider qui a le droit de voir quoi.
Les concepteurs d'interfaces ont, au fil des années, dégagé trois grands modèles de navigation. Tu les connais déjà comme utilisateur — ici, on va leur donner un nom précis, parce que ce sont exactement les briques qu'Expo Router te donne.
La pile (stack) : on empile, on dépile
Imagine une pile d'assiettes. Quand tu ouvres un écran de détail (par exemple, tu touches un exercice pour voir sa fiche), tu poses une nouvelle assiette par-dessus la précédente. Quand tu appuies sur « retour », tu retires l'assiette du dessus et tu retrouves celle d'en dessous, exactement dans l'état où tu l'avais laissée. En informatique, cette structure « dernier entré, premier sorti » s'appelle une pile (en anglais stack).
C'est précisément ce que tu vis des dizaines de fois par jour : Accueil → tu touches un exercice → fiche de l'exercice (empilée) → tu touches « commencer » → écran de séance (empilé encore). Le bouton retour, en haut à gauche, n'est rien d'autre que l'action « dépile l'écran du dessus ». La pile se souvient de tout le chemin parcouru. C'est ce souvenir qui rend le retour naturel et qui te ramène pile là où tu en étais.
Deux gestes différents sur une pile. Empiler (« push ») ajoute un écran par-dessus : on peut revenir en arrière. Remplacer (« replace ») échange l'écran du dessus par un autre : il n'y a plus de retour vers le précédent, car on l'a jeté. Cette distinction n'est pas un détail technique — elle change ce que fait le bouton retour, donc l'expérience entière. On y revient en détail plus bas, parce que ton app s'en sert à un moment crucial.
Les onglets (tabs) : plusieurs piles en parallèle
Les onglets, c'est la barre en bas de l'écran avec ses 3, 4 ou 5 icônes. Le modèle mental est différent de la pile : ici, il n'y a pas un « avant » et un « après », mais plusieurs destinations parallèles qui coexistent. Accueil, Séance, Exercices, Progrès : ce ne sont pas des étapes d'un parcours, ce sont quatre mondes côte à côte. Tu sautes de l'un à l'autre instantanément, et — point clé — chaque onglet conserve son état quand tu le quittes. Si tu fais défiler la liste des exercices, que tu passes à Progrès, puis que tu reviens aux Exercices, tu retrouves ton défilement là où il était.
Une bonne façon de se le représenter : chaque onglet contient sa propre petite pile. Tu peux empiler des écrans à l'intérieur de l'onglet Exercices (liste → fiche d'un exercice) sans que cela touche à l'onglet Progrès. Les onglets sont une navigation persistante et horizontale ; la pile est une navigation profonde et verticale. Les vraies apps combinent les deux : une barre d'onglets en bas, et dans chaque onglet, des écrans qui s'empilent.
La modale : un écran qui surgit par-dessus tout
Le troisième modèle, c'est la modale : un écran qui glisse depuis le bas (ou apparaît en superposition) et qui se place au-dessus de tout le reste, souvent pour une tâche brève et ciblée — confirmer une suppression, choisir une valeur, remplir un petit formulaire. La modale interrompt délibérément le flux : tant qu'elle est là, le reste de l'app attend. On la ferme en la balayant vers le bas ou avec un bouton « Annuler / Fermer ». Techniquement, une modale n'est qu'un écran de pile présenté avec une animation et un comportement particuliers — mais conceptuellement, son rôle est de dire « occupe-toi de ça maintenant, puis on reprend où on en était ».
Le bon modèle mental pour réunir tout ça, c'est un arbre. À la racine, un conteneur (souvent une pile). Certaines branches sont elles-mêmes des conteneurs (un groupe d'onglets, par exemple), et ces onglets contiennent à leur tour des écrans ou d'autres piles. Naviguer, c'est se déplacer dans cet arbre : descendre dans une branche (empiler), remonter (dépiler), sauter d'une grande branche à l'autre (changer d'onglet). Garde cette image en tête : tout le reste du module n'est qu'une façon de dessiner cet arbre avec des dossiers et des fichiers.
Le routage par fichiers : la structure EST la carte
Voici l'idée centrale d'Expo Router, et elle mérite qu'on s'y arrête, parce qu'elle est à la fois
simple et profonde. Dans beaucoup de systèmes de navigation classiques (l'ancien React Navigation, par
exemple), tu déclarais tes écrans dans un gros fichier de configuration : « voici l'écran Accueil, il
s'appelle Home, voici l'écran Réglages, il s'appelle Settings… » Ce fichier
devenait vite une liste à rallonge, déconnectée du code réel, qu'il fallait maintenir à la main.
Le routage par fichiers renverse la logique : il n'y a pas de fichier de configuration des
routes du tout. À la place, c'est l'arborescence du dossier app/
qui décrit la navigation. Chaque fichier .tsx que tu y poses devient une route, c'est-à-dire
un écran accessible par une « adresse » (une URL interne). app/settings.tsx devient l'écran
/settings. app/(tabs)/progress.tsx devient l'écran de l'onglet Progrès. La
carte de navigation, tu la lis simplement en regardant l'arbre des dossiers.
Le routage par fichiers est né sur le web. Sur un site, l'URL /blog/article
correspondait historiquement à un fichier blog/article.html sur le serveur : le chemin du
fichier était l'adresse de la page. Des frameworks modernes comme Next.js,
Remix ou SvelteKit ont repris ce principe pour les applications web. Expo Router l'a ensuite porté sur
le mobile. C'est pour cela que tu retrouves un vocabulaire « web » (route, URL, lien) au cœur d'une app
React Native — et c'est une excellente nouvelle pour toi : ce que tu apprends ici se transposera
presque tel quel le jour où tu toucheras au web.
Pourquoi ce choix est-il si apprécié ? Trois raisons concrètes :
- Pas de configuration séparée à maintenir. Tu ne risques jamais d'oublier de « brancher » un écran : le simple fait que le fichier existe le rend accessible. Créer un écran et le router, c'est un seul et même geste.
- Découvrabilité. N'importe qui (toi dans six mois, un nouveau coéquipier) comprend la structure de l'app en parcourant un dossier. La carte est auto-documentée. Pas besoin de chercher où sont déclarées les routes : elles sont là où on s'y attend.
- Convention plutôt que configuration. Tout le monde range les écrans pareil, donc on se repère vite d'un projet à l'autre. Moins de décisions arbitraires, moins de débats, plus de temps pour le vrai travail.
Les fichiers spéciaux à reconnaître
Le routage par fichiers ne se limite pas à « un fichier = un écran ». Quelques noms de fichiers ont une signification spéciale qu'il faut savoir lire. Ce sont les conventions qui te permettent de dessiner l'arbre de navigation entier. Apprends ce tableau : c'est l'alphabet d'Expo Router.
| Nom | Signification |
|---|---|
index.tsx | L'écran « racine » d'un dossier, comme la page d'accueil d'un site. app/(tabs)/index.tsx est l'écran affiché quand on arrive sur le groupe d'onglets, sans rien préciser de plus. |
_layout.tsx | Un emballage partagé par tous les écrans du dossier : c'est lui qui décide si ce dossier est une pile, des onglets, et qui peut ajouter une garde, des bannières, un en-tête. Le tiret bas en tête de nom signifie « ceci n'est PAS une page, c'est une enveloppe ». |
[id].tsx | Une route dynamique : les crochets veulent dire « valeur variable ». /exercise/42 et /exercise/99 utilisent le même fichier ; la partie variable (42, 99) est lue à l'intérieur de l'écran. |
(dossier) | Un groupe : les parenthèses organisent les fichiers et permettent de leur donner un _layout commun, sans apparaître dans l'URL. Purement pour ranger. |
+not-found.tsx | L'écran affiché quand aucune route ne correspond — l'équivalent de la page 404 d'un site. Le + marque un fichier « spécial » du framework. |
Remarque le motif : les caractères « décoratifs » (_, [], (),
+) sont autant de signaux. Dès que tu vois l'un d'eux dans un nom de
fichier sous app/, tu sais que ce fichier joue un rôle de structure plutôt que d'être un
simple écran. Lire la navigation d'une app inconnue revient alors à parcourir app/ en
décodant ces signaux.
Les groupes : (auth), (app) et (tabs)
Les groupes — ces dossiers entre parenthèses — sont sans doute la convention la plus déroutante au
premier abord, alors prenons le temps. Un groupe ne crée aucun segment d'URL. Son seul
rôle est de rassembler des écrans qui partagent quelque chose : le plus souvent, un
même _layout.tsx, donc un même comportement (la même garde, la même barre d'onglets, le
même en-tête).
Ton app sépare très nettement deux univers : les écrans publics (où l'on a le droit
d'aller sans être connecté : connexion, inscription, confirmation de compte) et les écrans
privés (tout le reste — il faut être connecté). C'est exactement la frontière que
dessinent les groupes (auth) et (app) :
app/
├── (auth)/ ← écrans publics : sign-in, sign-up, confirm-account ┐ parenthèses =
│ ├── sign-in.tsx → URL réelle : /sign-in │ regroupement,
│ └── sign-up.tsx → URL réelle : /sign-up │ PAS dans l'URL
├── (app)/ ← écrans privés (connexion requise) │
│ ├── (tabs)/ ← les 4 onglets du bas ┘
│ │ ├── index.tsx → / (accueil)
│ │ ├── workout.tsx → /workout
│ │ ├── exercises.tsx → /exercises
│ │ └── progress.tsx → /progress
│ ├── exercise/[id].tsx → /exercise/42 (route dynamique)
│ └── settings.tsx → /settings
└── _layout.tsx ← emballage racine de toute l'app
Le (auth) n'apparaît pas dans l'URL : sign-up.tsx donne bien
/sign-up, et non /(auth)/sign-up. De même, l'accueil est à / et
non à /(app)/(tabs)/. Les parenthèses servent uniquement à regrouper des écrans
qui partagent un même _layout.tsx.
Pourquoi se donner cette peine plutôt que tout poser à plat ? Parce que le groupe te permet
d'appliquer un comportement commun à tout un sous-arbre d'un coup. Tous les écrans de
(app) doivent être protégés par l'authentification : au lieu de répéter ce contrôle écran
par écran, on l'écrit une seule fois dans le _layout.tsx du groupe. Tous
les écrans de (tabs) doivent partager la même barre d'onglets : on la déclare une fois dans
le _layout.tsx du groupe (tabs). Le groupe, c'est l'endroit où l'on factorise
« ce que ces écrans ont en commun ». Et comme il n'apparaît pas dans l'URL, on peut réorganiser le
rangement interne sans casser les adresses.
La garde d'authentification, en vrai
On arrive au cœur du sujet, et à l'un des plus beaux exemples de la puissance de cette architecture. Le
_layout.tsx du groupe (app) a une mission : protéger tous les écrans
privés. Le principe est d'une simplicité désarmante — un layout est un composant, comme tu l'as
vu dans le module sur le modèle de React, et un composant retourne du JSX. Eh bien, il suffit
que ce layout regarde si l'utilisateur est connecté et, sinon, qu'il retourne un
<Redirect> au lieu des écrans privés. Voici le vrai code, à peine simplifié :
export default function AppLayout() {
// On lit l'état d'authentification depuis le store global (voir le module
// sur l'état global / Zustand). isAuthenticated est un booléen : connecté ou non.
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
// LA GARDE. Pas connecté ? On REDIRIGE vers l'écran de connexion,
// et on n'affiche AUCUN écran privé — la fonction s'arrête là, ici même.
if (!isAuthenticated) {
return <Redirect href="/sign-in" />;
}
// Connecté → on rend la pile d'écrans privés. Chaque Stack.Screen
// correspond à un dossier/fichier sous (app)/.
return (
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(tabs)" />
<Stack.Screen name="exercise" />
<Stack.Screen name="plans" />
<Stack.Screen name="workout" />
<Stack.Screen name="settings" />
</Stack>
);
}
Tout repose sur le modèle de React : ce layout est un composant qui retourne du JSX. S'il retourne
<Redirect>, l'utilisateur file vers la connexion. Sinon il retourne un
<Stack> — une pile d'écrans empilables — qui devient le conteneur de tout le
sous-arbre privé.
Prends une seconde pour mesurer ce qui se passe ici. Ce if (!isAuthenticated) de trois
lignes protège des dizaines d'écrans à la fois. Le groupe (app) contient
les onglets, les fiches d'exercices, les séances, les réglages — absolument tout ce qui est privé. Comme
ce layout enveloppe tout ce sous-arbre, aucun de ces écrans ne peut s'afficher tant que la garde
n'a pas dit oui. Tu n'as pas besoin d'ajouter un contrôle dans chaque écran : le simple fait
d'être rangé sous (app) rend un écran protégé. C'est l'arbre de navigation qui fait
le travail de sécurité.
La vraie garde fait un peu plus encore. En plus de la redirection, le layout affiche des
bannières contextuelles au-dessus de la pile : une bannière si l'email n'est pas
vérifié (emailVerified === false), une pastille discrète d'état de synchronisation
hors-ligne, et un rappel quotidien quand des séances n'ont pas encore été envoyées au serveur. Le
layout n'est donc pas qu'un videur à l'entrée : c'est aussi le bon endroit pour tout ce qui doit être
présent sur l'ensemble des écrans privés. Le principe reste le même — un composant qui
décide quoi afficher autour de la pile.
Et d'où vient cette information isAuthenticated ? Du store global (vu dans
le module sur l'état global avec Zustand). C'est ce qui rend la garde vivante : quand
l'utilisateur se connecte ou se déconnecte, le store change, le layout se réexécute
automatiquement (c'est le ré-affichage réactif de React), et la garde se ré-évalue. Connexion réussie →
isAuthenticated passe à true → le <Redirect> n'est plus
retourné → les écrans privés apparaissent. Déconnexion → l'inverse, instantanément. Le booléen, lui,
est mis à jour à partir de la session Supabase (voir le module sur l'authentification
et Supabase) : c'est Supabase qui sait si la session est valide, et le store qui répercute cette vérité
dans toute l'app. La navigation, l'état global et le backend forment ici une chaîne cohérente.
Il y a un piège de synchronisation derrière cette élégance : au démarrage, l'app doit d'abord
restaurer la session (vérifier auprès de Supabase si l'utilisateur était déjà
connecté) avant de pouvoir trancher. Si la garde s'exécutait trop tôt, elle verrait
isAuthenticated === false par défaut et redirigerait à tort un utilisateur pourtant
connecté. C'est pourquoi le layout racine n'affiche rien tant que tout n'est pas
prêt (polices chargées, session restaurée). Quand ce _layout.tsx de (app)
se monte enfin, isAuthenticated est déjà fiable. Retiens l'idée : une garde ne
vaut que si l'information qu'elle lit est déjà settled.
Stack vs Tabs : choisir le bon conteneur
On a vu en théorie la différence entre pile et onglets. Voyons-la maintenant dans ton code. Le layout
de (app) ci-dessus retourne une Stack : à ce niveau, on empile des
destinations majeures (le bloc d'onglets, un exercice, une séance, les réglages). Mais l'un de ces
écrans de pile, (tabs), est lui-même un groupe d'onglets. Voici son
layout :
<Tabs screenOptions={{ headerShown: false /* ... styles, couleurs ... */ }}>
<Tabs.Screen
name="index" // → l'onglet Accueil (le fichier index.tsx)
options={{
title: 'Home',
tabBarIcon: ({ color }) => <Ionicons name="home" color={color} />,
}}
/>
<Tabs.Screen
name="workout" // → l'onglet Séance (workout.tsx)
options={{ title: 'Workout', tabBarIcon: ({ color }) =>
<Ionicons name="checkmark-circle" color={color} /> }}
/>
<Tabs.Screen name="exercises" options={{ title: 'Exercises', /* ... */ }} />
<Tabs.Screen name="progress" options={{ title: 'Progress', /* ... */ }} />
</Tabs>
Chaque <Tabs.Screen name="..."> pointe vers un fichier du dossier (tabs)/
(index.tsx, workout.tsx…) et configure son onglet : titre et icône. Le
tabBarIcon est une petite fonction qui reçoit la couleur courante (différente selon que
l'onglet est actif ou non) et retourne l'icône — encore et toujours le modèle props → JSX.
Note la structure imbriquée : une Stack à la racine du privé, et dans l'un de
ses écrans, des Tabs. C'est l'arbre de navigation qui se concrétise. Cela permet, par exemple,
d'empiler l'écran d'une séance en cours (un Stack.Screen name="workout" au niveau
de (app)) par-dessus toute la barre d'onglets : la séance prend tout
l'écran, la barre disparaît, et le retour ramène aux onglets. Si la séance était un onglet, elle ne
pourrait jamais masquer la barre.
Comment choisir, alors, entre une Stack et des Tabs pour un ensemble d'écrans donné ? La question à se poser est : ces écrans sont-ils des étapes d'un parcours ou des destinations parallèles ?
| Choisis une Stack quand… | Choisis des Tabs quand… |
|---|---|
| Les écrans forment un fil : on avance, on revient. | Les écrans sont des sections de même niveau, sans ordre. |
| On veut un bouton retour et un historique. | On veut sauter instantanément de l'un à l'autre. |
| Ex. : liste → fiche d'exercice → séance. | Ex. : Accueil / Séance / Exercices / Progrès. |
| L'écran enfant doit pouvoir cacher le parent. | Chaque section doit conserver son état entre les visites. |
Dans la pratique, presque toutes les apps mélangent les deux exactement comme Halterofit : une barre d'onglets pour les grandes sections, et à l'intérieur de chaque section (ou par-dessus elle), des piles pour creuser dans le détail.
Naviguer dans le code : push, replace et paramètres
Jusqu'ici on a décrit la structure. Voyons maintenant comment on déclenche une
navigation depuis le code — par exemple, quand l'utilisateur touche un bouton. Expo Router te donne un
objet router avec deux méthodes essentielles, qui correspondent exactement aux deux gestes
de pile vus tout au début :
import { router } from 'expo-router';
// EMPILER : on ajoute /settings par-dessus. Le bouton retour ramènera ici.
router.push('/settings');
// REMPLACER : on échange l'écran courant contre /sign-in.
// Plus de retour vers l'écran d'où l'on vient — il a été jeté de la pile.
router.replace('/sign-in');
push et replace ne diffèrent que par leur effet sur l'historique de la pile :
l'un conserve le précédent, l'autre l'efface. Le choix dépend entièrement de ce que tu veux que le
bouton retour fasse ensuite.
Le cas le plus parlant, tu l'as déjà rencontré dans le module sur l'inscription. À la fin du formulaire
d'inscription, le code n'utilise pas push mais replace :
// Inscription réussie → on envoie vers l'écran de confirmation,
// en transmettant l'email au passage via params.
router.replace({
pathname: '/(auth)/confirm-account',
params: { email: email.trim() },
});
On remplace l'écran d'inscription au lieu d'empiler. Résultat : depuis la confirmation, le bouton retour ne ramènera pas au formulaire qu'on vient juste de valider — ce qui n'aurait aucun sens. On veut une marche en avant, sans retour en arrière possible.
Remarque aussi le champ params : c'est le canal standard pour transmettre une
donnée d'un écran au suivant. Ici, on passe l'email saisi pour que l'écran de confirmation
sache à quelle adresse le code de vérification a été envoyé. L'écran de destination lit ce paramètre avec
le hook useLocalSearchParams :
import { useLocalSearchParams } from 'expo-router';
export default function ConfirmAccount() {
// On récupère ce qui a été passé dans params lors de la navigation.
const { email } = useLocalSearchParams<{ email: string }>();
// → email contient bien l'adresse transmise par l'écran d'inscription.
// ...
}
useLocalSearchParams est le pendant lecteur de params : ce que l'un envoie,
l'autre le reçoit. C'est aussi par lui qu'une route dynamique récupère sa partie variable.
Routes dynamiques et liens vérifiés
Revenons sur les routes dynamiques [id].tsx, car c'est exactement le même mécanisme de
paramètre, mais cette fois la valeur est dans l'URL. Un seul fichier
exercise/[id].tsx sert pour tous les exercices. Quand on navigue vers
/exercise/42, l'écran lit la valeur 42 ainsi :
// Fichier : app/(app)/exercise/[id].tsx
export default function ExerciseDetail() {
// Le nom de la variable (id) correspond au nom entre crochets du fichier.
const { id } = useLocalSearchParams<{ id: string }>();
// → id vaut "42" pour /exercise/42, "99" pour /exercise/99, etc.
// On va ensuite chercher les données de CET exercice avec son id.
}
Le crochet du nom de fichier ([id]) et la clé lue (id) doivent porter le même
nom : c'est ce qui relie l'URL au code. Un fichier, une infinité d'écrans concrets.
C'est élégant, mais cela soulève une crainte légitime : si les adresses sont des chaînes de caractères
('/exercise/42', '/settings'), que se passe-t-il le jour où tu fais une faute
de frappe — '/setttings' ? Avec de simples chaînes, rien ne te préviendrait avant le
plantage à l'exécution. C'est là qu'interviennent les typed routes (routes typées) :
Expo Router peut générer automatiquement les types de toutes tes routes à partir de
l'arborescence de fichiers. TypeScript connaît alors la liste exacte des adresses valides, et
signale l'erreur à la compilation si tu écris un lien qui ne correspond à aucun écran.
Le compilateur devient un garde-fou : impossible de naviguer vers un écran qui n'existe pas sans que ton
éditeur ne te le dise immédiatement, avant même de lancer l'app.
Les liens cassés sont parmi les bugs les plus pénibles : ils ne se voient pas en lisant le code, ils n'éclatent qu'au moment précis où l'utilisateur touche le bouton. Les typed routes transforment une erreur d'exécution (découverte tard, par un humain, peut-être en production) en une erreur de compilation (découverte tout de suite, par la machine, dans ton éditeur). C'est exactement l'esprit de TypeScript appliqué à la navigation : déplacer la détection des fautes le plus tôt possible.
Les deep links : entrer dans l'app par une porte précise
Jusqu'ici, on a supposé que l'utilisateur navigue depuis l'intérieur de l'app. Mais il existe un autre cas, plus subtil : ouvrir un écran précis depuis l'extérieur, via un lien. C'est ce qu'on appelle un deep link (lien profond) — « profond » parce qu'il ne te dépose pas bêtement sur l'accueil, mais te conduit directement au bon endroit dans l'arborescence, comme si tu avais déjà fait tout le chemin.
L'exemple le plus courant, et celui qui te concerne directement, c'est la réinitialisation de mot de passe. L'utilisateur a oublié son mot de passe ; il reçoit un email avec un bouton « Réinitialiser ». Ce bouton contient un lien spécial qui, une fois touché, ouvre ton app et atterrit pile sur l'écran de nouveau mot de passe, en transportant au passage le jeton de sécurité qui prouve que la demande est légitime. Sans deep link, tu devrais demander à l'utilisateur de rouvrir l'app à la main et de retrouver le bon écran — une friction qui ferait abandonner beaucoup de monde.
Techniquement, c'est expo-linking qui s'en occupe. On associe à l'app un
schéma d'URL (par exemple halterofit://) et/ou des liens web, puis le système
d'exploitation sait que tout lien de cette forme doit réveiller ton app. Et — c'est là que la boucle se
referme magnifiquement — Expo Router réutilise ta carte de fichiers pour décider quel
écran ouvrir. Comme l'arborescence est la navigation, le lien halterofit://exercise/42
mène naturellement à app/(app)/exercise/[id].tsx avec id = 42. Tu n'as pas une
deuxième carte à maintenir : la même structure sert pour la navigation interne et pour les liens
externes.
Le flux de récupération de compte repose sur cette mécanique. Supabase (vu dans le module sur
l'authentification) envoie l'email avec un lien de redirection ; quand l'utilisateur le touche,
expo-linking ouvre l'app sur l'écran adéquat, le jeton est récupéré, et la session de
réinitialisation s'établit — le tout sans que la personne ait à naviguer manuellement. C'est un bel
exemple de la collaboration entre navigation, deep linking et
backend.
Reprenons le code de fin d'inscription vu plus haut :
router.replace({
pathname: '/(auth)/confirm-account',
params: { email: email.trim() },
});
Questions : (1) Pourquoi replace plutôt que push ici ?
(2) Comment l'écran confirm-account récupérera-t-il l'email ?
(3) Pourquoi écrit-on /(auth)/confirm-account avec le groupe entre parenthèses, alors
qu'on a dit que le groupe n'apparaît pas dans l'URL ?
Voir le corrigé
(1) replace remplace l'écran d'inscription au lieu d'empiler par-dessus.
Ainsi, depuis l'écran de confirmation, le bouton « retour » ne ramène pas au formulaire d'inscription
(qu'on vient justement de valider) — ce qui n'aurait pas de sens et pourrait même créer un état
incohérent. On veut une navigation « vers l'avant », sans marche arrière.
(2) Via useLocalSearchParams() : l'email passé dans params
devient lisible dans l'écran de destination (const { email } = useLocalSearchParams()).
C'est le canal standard pour transmettre une donnée d'un écran au suivant.
(3) Subtilité importante : le groupe n'apparaît pas dans l'URL visible, mais
on peut tout de même l'écrire dans le pathname pour lever une ambiguïté
et indiquer précisément quel sous-arbre on vise. Expo Router accepte le segment de groupe comme une
indication de chemin ; il ne l'affichera simplement pas comme un segment d'adresse. C'est une aide à
la désambiguïsation, pas une contradiction.
1. Comment crée-t-on une nouvelle route avec Expo Router ?
En créant un fichier dans app/. Le routage est basé sur les fichiers : pas de configuration séparée à brancher. Le fichier existe → l'écran existe.
2. Quelle est la différence de fond entre une Stack et des Tabs ?
Une Stack empile des écrans dans un parcours (on avance, on revient avec le bouton retour). Des Tabs offrent des destinations parallèles et persistantes : on saute de l'une à l'autre instantanément, et chaque onglet conserve son état. On les combine : des onglets, et dans chacun, des piles.
3. Que veut dire un nom de dossier entre parenthèses, comme (app) ?
C'est un groupe : il rassemble des écrans (et leur _layout partagé) sans ajouter de segment à l'URL. Il sert à factoriser un comportement commun — par exemple, la garde d'authentification pour tout (app).
4. Que fait return <Redirect href="/sign-in" /> dans un layout ?
Il redirige immédiatement vers /sign-in au lieu d'afficher les écrans du layout. Puisqu'un layout est un composant qui retourne du JSX, retourner un <Redirect> protège d'un coup tout son sous-arbre : c'est le mécanisme de la garde d'authentification.
5. Pourquoi un deep link de réinitialisation de mot de passe « sait-il » où atterrir dans l'app ?
Parce qu'Expo Router réutilise la carte de fichiers. Comme l'arborescence est la navigation, un lien comme halterofit://... mène au fichier correspondant sans qu'on ait à maintenir une seconde carte. expo-linking ouvre l'app, et la structure des dossiers fait le reste.
Naviguer, c'est se déplacer dans un arbre d'écrans fait de piles (on empile/dépile,
d'où le retour), d'onglets (destinations parallèles persistantes) et de modales. Dans Expo Router, cet
arbre est dessiné par les fichiers de app/ : un fichier = un écran.
_layout.tsx = emballage partagé, [id].tsx = route dynamique,
(groupe) = organisation invisible dans l'URL, index.tsx = écran racine d'un
dossier, +not-found.tsx = page 404. Une garde d'auth, c'est juste un layout qui retourne
<Redirect> si on n'est pas connecté — il protège tout son sous-arbre. Pour naviguer
dans le code : router.push (empile) vs router.replace (remplace, pas de
retour), params pour transmettre, useLocalSearchParams pour lire. Les typed
routes vérifient tes liens à la compilation ; les deep links (expo-linking) ouvrent un
écran précis depuis l'extérieur.
Oublier que (groupe) n'est PAS dans l'URL. On voit un dossier
(auth) et on s'attend à /(auth)/sign-in ; or l'adresse réelle est
/sign-in. Si tu cherches pourquoi un lien ne marche pas, vérifie d'abord si tu n'as pas
ajouté un segment de groupe de trop dans l'URL visible.
Utiliser push au lieu de replace après une étape sans retour.
Après l'inscription, la connexion ou la déconnexion, push laisse l'ancien écran dans la
pile : l'utilisateur peut alors « revenir » sur un formulaire déjà soumis ou sur une page qu'il ne
devrait plus voir. Quand l'écran précédent ne doit plus jamais réapparaître, c'est replace.
Le routage par fichiers vient du web et y règne (Next.js, Remix, SvelteKit fonctionnent pareil).
Apprendre la logique « arborescence = navigation » te servira directement le jour où tu toucheras au
web — y compris la future partie apps/web de ton propre projet. Mieux : les notions de
pile, d'onglets et de modale sont universelles —
tu les retrouveras à l'identique sur iOS natif, Android natif, ou n'importe quel framework d'interface.
Et l'idée d'une garde (un point d'entrée unique qui autorise ou redirige) est un
patron qu'on revoit partout en sécurité applicative, du middleware web aux intercepteurs d'API.