Skip to Content

Chapitre 5.3 — Routes dynamiques & paramètres

Où on en est : la charpente tabs est posée. L’onglet Thèmes liste les thèmes — il faut maintenant l’écran de détail : UN fichier pour TOUS les thèmes.

⏱️ TL;DRapp/theme/[slug].tsx matche /theme/symfony, /theme/php… Le paramètre se lit avec useLocalSearchParams. Les typed routes vérifient vos href à la compilation. On ajoute une pile locale à l’onglet Thèmes (liste → détail SOUS la barre d’onglets), et on passe les paramètres de session au quiz (/quiz?theme=symfony&count=10).

🎯 Objectifs

  • Créer la route dynamique du détail d’un thème et lire ses params.
  • Donner à l’onglet Thèmes sa propre pile (liste → détail, barre visible).
  • Typer et valider les paramètres (les params sont des strings !).
  • Transmettre la configuration d’une session au quiz.

La route dynamique

app/(tabs)/themes/ ├── _layout.tsx → Stack local à l'onglet ├── index.tsx → liste des thèmes "/themes" └── [slug].tsx → détail d'un thème "/themes/symfony", "/themes/php"…

L’onglet Thèmes devient un dossier avec sa pile : naviguer vers un détail garde la barre d’onglets visible (on reste « chez » Thèmes) — contrairement au quiz plein écran.

// app/(tabs)/themes/_layout.tsx import { Stack } from 'expo-router' export default function ThemesLayout() { return ( <Stack screenOptions={{ headerStyle: { backgroundColor: '#0b1216' }, headerTintColor: '#e2e8f0', headerShadowVisible: false, contentStyle: { backgroundColor: '#0b1216' }, }} /> ) }
// app/(tabs)/themes/index.tsx — la liste (FlatList du chapitre 3.4 + ThemeCard) <FlatList data={themes} keyExtractor={t => t.slug} renderItem={({ item }) => ( <Link href={`/themes/${item.slug}`} asChild> <Pressable className="active:opacity-85"> <ThemeCard title={item.title} questionCount={item.count} emoji={item.emoji} /> </Pressable> </Link> )} contentContainerClassName="gap-3 p-4" />

Lire les paramètres

// app/(tabs)/themes/[slug].tsx import { Stack, useLocalSearchParams } from 'expo-router' export default function ThemeDetailScreen() { const { slug } = useLocalSearchParams<{ slug: string }>() const theme = getThemeBySlug(slug) // lib/ — données en dur pour l'instant if (!theme) return <NotFoundTheme /> return ( <> <Stack.Screen options={{ title: theme.title }} /> <Screen scroll> <AppText variant="title">{theme.emoji} {theme.title}</AppText> <AppText variant="caption">{theme.count} questions · record : 870 pts</AppText> {/* choix du nombre de questions, difficulté… puis : */} <Link href={{ pathname: '/quiz', params: { theme: theme.slug, count: 10 } }} asChild> <Button label="Lancer une session" /> </Link> </Screen> </> ) }

Trois choses à retenir :

  • useLocalSearchParams lit les params de CET écran (son cousin useGlobalSearchParams réagit aussi aux changements pendant que l’écran est sous la pile — rare).
  • Le Stack.Screen options dynamique : le titre du header vient des données.
  • Le href objet (pathname + params) : la manière propre de passer des query params — ici la config de session vers le quiz (/quiz?theme=symfony&count=10).

⚠️ Les params sont des STRINGS

Comme les query params du web : count: 10 ressort "10". Et useLocalSearchParams type par défaut en string | string[]. La règle du cours : valider aux frontières

// app/quiz.tsx — lecture défensive des params de session const params = useLocalSearchParams<{ theme?: string; count?: string }>() const theme = typeof params.theme === 'string' ? params.theme : 'melange' const count = Math.min(50, Math.max(5, Number(params.count) || 10))

Un deep link (chapitre 5.5) pourra ouvrir /quiz avec des params arbitraires (malformés, absents) : l’écran doit avoir des défauts sûrs. C’est le même réflexe que valider searchParams dans une page Next.js — en plus important, car pas de serveur pour assainir avant vous.

Typed routes — le filet

Avec experiments.typedRoutes (activé chapitre 2.4), Expo Router génère les types de toutes vos routes :

<Link href="/themes/symfony" /> // ✅ <Link href="/theme/symfony" /> // ❌ erreur TS : route inexistante (singulier !) <Link href={{ pathname: '/quiz', params: { theme: 's' } }} /> // ✅ params typés aussi

Les fautes de frappe de navigation meurent à la compilation. Les segments dynamiques acceptent n’importe quelle valeur du bon shape (/themes/nimportequoi compile — d’où la validation runtime ci-dessus : les types vérifient la forme, pas l’existence).

💡 Pour un dev Next.js[slug].tsx = [slug]/page.tsx, useLocalSearchParams = useParams() + useSearchParams() réunis. Pas d’équivalent de generateStaticParams (rien n’est généré : tout est résolu à l’exécution), et pas de data fetching serveur dans la route — les données viennent des hooks (TanStack Query en Partie 7).

✏️ Exercices

1. Implémentez lib/themes.ts : le type Theme (slug, title, emoji, count), le tableau en dur (Bootstrap, Next.js, PHP, Symfony), getThemeBySlug. Puis branchez liste + détail.

✅ Solution

// lib/themes.ts export type Theme = { slug: string; title: string; emoji: string; count: number } export const themes: Theme[] = [ { slug: 'bootstrap', title: 'Bootstrap', emoji: '🅱️', count: 80 }, { slug: 'nextjs', title: 'Next.js', emoji: '▲', count: 120 }, { slug: 'php', title: 'PHP 8.5', emoji: '🐘', count: 140 }, { slug: 'symfony', title: 'Symfony 7', emoji: '🎼', count: 130 }, ] export const getThemeBySlug = (slug: string) => themes.find(t => t.slug === slug)

Pure TS dans lib/ (notre règle) — remplacé en P7 par le fetch du manifest GitHub, sans changer les écrans.

2. Gérez le cas /themes/inexistant : un écran « thème introuvable » propre avec retour à la liste. Bonus : le fichier de convention +not-found.tsx.

✅ Solution

Dans [slug].tsx, le if (!theme) rend un composant avec message + <Link href="/themes">. Bonus : app/+not-found.tsx attrape TOUTES les routes inconnues de l’app (deep link mort, etc.) — un écran 404 global avec bouton retour accueil. Les deux niveaux se complètent : +not-found pour les routes sans fichier, la garde pour les slugs sans données.

3. Le détail d’un thème doit proposer 5/10/20 questions. Implémentez le sélecteur et le passage au quiz — puis vérifiez dans le quiz que count=abc (URL forgée) retombe sur 10.

✅ Solution

Trois Badge/boutons mettant count dans un useState local, puis href={{ pathname: '/quiz', params: { theme: slug, count } }}. Côté quiz, la lecture défensive (Number(params.count) || 10 borné 5-50) transforme "abc" en 10. Test : naviguer manuellement (barre Metro npx uri-scheme au ch. 5.5, ou modifier le Link) et observer.

🧠 Quiz

1. Quel fichier matche /themes/php, et comment lit-il « php » ?

Réponse

app/(tabs)/themes/[slug].tsx, via const { slug } = useLocalSearchParams<{ slug: string }>().

2. Pourquoi donner un Stack local à l’onglet Thèmes ?

Réponse

Pour que liste → détail s’empile dans l’onglet (barre visible, back local à la pile de l’onglet), au lieu de recouvrir toute l’app comme le quiz.

3. Type réel de params.count après /quiz?count=10 ?

Réponse

"10" (string) — tous les params arrivent en string (ou string[]). Convertir ET valider avec défauts sûrs (Number(x) || 10, bornes).

4. Que garantissent les typed routes — et que ne garantissent-elles pas ?

Réponse

Elles garantissent que le chemin a la forme d’une route existante (faute de frappe = erreur TS). Elles ne garantissent PAS que la donnée existe (/themes/xyz compile) → garde runtime.

Réponse

La forme objet : href={{ pathname: '/quiz', params: { theme: 'php', count: 10 } }} — échappement et types gérés.


👉 Chapitre suivant : 5.4 — Modals & navigation programmatique — le flux complet d’une partie : lancer, abandonner, terminer, rejouer.