7 minutes de lecture

Expo Router pour les devs Next.js : le routing mobile que tu connais déjà

#Développement#React Native#Routing#Code#Mobile
Expo Router pour les devs Next.js : le routing mobile que tu connais déjà
FFranck NIAT
|30 mars 2026

De Next.js à Expo Router : tu connais déjà la moitié

Tu bosses avec Next.js depuis un moment. L'App Router, les layouts, le file-based routing — tout ça, c'est devenu une seconde nature. Et maintenant tu veux te mettre au mobile. React Native t'intéresse, mais tu redoutes de repartir de zéro. J'ai travaillé sur de nombreux projets Next.js et quand je me lançais sur Expo, je me suis rendu compte que je connaissais déjà la moitié du travail.

Expo Router est un système de routing pour React Native qui s'est directement inspiré du Next.js App Router. Même philosophie, même structure de fichiers, même API de navigation. Dans cet article, on va passer en revue ce qui est identique, ce qui change légèrement, et les quelques points de friction à anticiper.


Le point de départ : créer un projet Expo Router

Avant tout, initialise un projet pour suivre les exemples :

bash
npx create-expo-app@latest mon-app --template tabs
cd mon-app
npx expo start

Ouvre le dossier app/ — tu vas immédiatement reconnaître quelque chose.


1. La structure de fichiers : presque identique

C'est la première chose qui frappe. Expo Router utilise exactement le même principe de file-based routing que Next.js.

Route souhaitéeNext.js App RouterExpo Router
/ (accueil)app/page.tsxapp/index.tsx
/aboutapp/about/page.tsxapp/about.tsx
/blog/[id]app/blog/[id]/page.tsxapp/blog/[id].tsx
Layout globalapp/layout.tsxapp/(root)/_layout.tsx

Les différences à noter :

  • La page racine s'appelle index.tsx et non page.tsx
  • Les layouts s'appellent _layout.tsx (underscore) et non layout.tsx
  • Les fichiers de route n'ont pas besoin d'être dans un sous-dossier

2. Les layouts : même concept, syntaxe légèrement différente

En Next.js, ton layout.tsx ressemble à ça :

tsx
// app/layout.tsx — Next.js
export default function RootLayout({
    children,
}: {
    children: React.ReactNode
}) {
    return (
        <html lang="fr">
            <body>
                <Header />
                {children}
                <Footer />
            </body>
        </html>
    )
}

En Expo Router, le _layout.tsx joue le même rôle de conteneur parent — mais la navigation native remplace le <html> :

tsx
// app/_layout.tsx — Expo Router
import { Stack } from "expo-router"
 
export default function RootLayout() {
    return (
        <Stack>
            <Stack.Screen name="index" options={{ title: "Accueil" }} />
            <Stack.Screen name="about" options={{ title: "À propos" }} />
        </Stack>
    )
}

Stack est le navigateur le plus basique : il empile les écrans les uns sur les autres, comme une navigation mobile classique avec un bouton "retour". Tu peux le remplacer par Tabs pour une navigation à onglets (comme dans Instagram ou Twitter).


3. La navigation : useRouter() est quasi copié-collé

C'est là que la ressemblance devient presque troublante. Compare :

Next.js

tsx
import Link from "next/link"
import { useRouter } from "next/navigation"
 
export default function Page() {
    const router = useRouter()
 
    return (
        <>
            {/* Navigation déclarative */}
            <Link href="/about">À propos</Link>
 
            {/* Navigation programmatique */}
            <button onClick={() => router.push("/blog/42")}>
                Voir l'article
            </button>
 
            <button onClick={() => router.back()}>Retour</button>
        </>
    )
}

Expo Router

tsx
import { Link, useRouter } from "expo-router"
import { Pressable, Text } from "react-native"
 
export default function Screen() {
    const router = useRouter()
 
    return (
        <>
            {/* Navigation déclarative */}
            <Link href="/about">À propos</Link>
 
            {/* Navigation programmatique */}
            <Pressable onPress={() => router.push("/blog/42")}>
                <Text>Voir l'article</Text>
            </Pressable>
 
            <Pressable onPress={() => router.back()}>
                <Text>Retour</Text>
            </Pressable>
        </>
    )
}

useRouter(), router.push(), router.back(), router.replace() — tout ça fonctionne exactement pareil. La seule différence visible : <button> devient <Pressable> et <p> devient <Text>.

Les routes dynamiques fonctionnent pareil aussi

tsx
// app/blog/[id].tsx
import { useLocalSearchParams } from "expo-router"
 
export default function BlogPost() {
    const { id } = useLocalSearchParams()
    // Équivalent de useParams() ou params.id en Next.js
 
    return <Text>Article #{id}</Text>
}

4. Les layouts imbriqués : groupes de routes

Expo Router supporte aussi les groupes de routes avec les parenthèses, exactement comme Next.js :

code
app/
├── (auth)/
│   ├── _layout.tsx    ← layout pour les pages auth
│   ├── login.tsx
│   └── register.tsx
├── (app)/
│   ├── _layout.tsx    ← layout pour l'app principale (tabs)
│   ├── index.tsx
│   └── profile.tsx
└── _layout.tsx        ← layout racine

Le layout (app)/_layout.tsx peut utiliser une navigation à onglets :

tsx
// app/(app)/_layout.tsx
import { Tabs } from "expo-router"
 
export default function AppLayout() {
    return (
        <Tabs>
            <Tabs.Screen name="index" options={{ title: "Accueil" }} />
            <Tabs.Screen name="profile" options={{ title: "Profil" }} />
        </Tabs>
    )
}

5. Ce qui change vraiment

OK, on a beaucoup parlé des similitudes. Voilà les points où tu vas devoir changer tes réflexes.

Adieu le CSS, bonjour StyleSheet

C'est sans doute le plus grand changement. Il n'y a pas de CSS en React Native. Tout se fait avec StyleSheet.create() :

tsx
import { StyleSheet, View, Text } from "react-native"
 
export default function Card() {
    return (
        <View style={styles.card}>
            <Text style={styles.title}>Mon titre</Text>
        </View>
    )
}
 
const styles = StyleSheet.create({
    card: {
        padding: 16,
        backgroundColor: "#ffffff",
        borderRadius: 8,
        // Pas de "box-shadow" CSS — on utilise :
        shadowColor: "#000",
        shadowOffset: { width: 0, height: 2 },
        shadowOpacity: 0.1,
        shadowRadius: 4,
        elevation: 3, // Android uniquement
    },
    title: {
        fontSize: 18,
        fontWeight: "600",
        color: "#111",
    },
})

Flexbox fonctionne, mais avec quelques différences :

  • flexDirection est column par défaut (et non row comme en CSS)
  • Pas de gap (il faut utiliser marginBottom sur les enfants... ou gap si tu cibles Expo SDK 49+)

Les primitives HTML n'existent pas

Tu n'as pas de <div>, <p>, <span>, <button>, <img>. À la place :

HTMLReact Native
<div><View>
<p>, <span>, <h1><Text>
<button><Pressable> ou <TouchableOpacity>
<img><Image>
<input><TextInput>
<ul><li><FlatList>

Pas de SSR

Expo Router est 100% client-side. Pas de getServerSideProps, pas de Server Components, pas de fetch() au moment du rendu serveur. Tout se fait côté client, souvent avec useEffect ou des librairies comme React Query ou SWR.

tsx
// Fetching de données en Expo Router
import { useEffect, useState } from "react"
 
export default function Screen() {
    const [data, setData] = useState(null)
 
    useEffect(() => {
        fetch("https://api.example.com/data")
            .then((r) => r.json())
            .then(setData)
    }, [])
 
    return <Text>{data?.title}</Text>
}

Deux features qui fonctionnent très bien avec Expo Router et qui te serviront vite.

Les modals natifs

tsx
// app/_layout.tsx
<Stack>
    <Stack.Screen name="index" />
    <Stack.Screen
        name="modal"
        options={{ presentation: "modal" }} // ← magie native
    />
</Stack>
tsx
// N'importe où dans l'app
router.push("/modal")

Expo Router gère les deep links automatiquement. Si ton app s'appelle monapp, le lien monapp://blog/42 ouvrira directement app/blog/[id].tsx avec id = 42. Zéro configuration supplémentaire.


Récap : ce que tu retiens en 30 secondes

Identique :

  • File-based routing dans le dossier app/
  • Routes dynamiques avec [id]
  • useRouter(), router.push(), router.back()
  • Composant <Link href="...">
  • Groupes de routes avec (nom)/
  • Layouts imbriqués

Change :

  • layout.tsx_layout.tsx
  • page.tsxindex.tsx pour la racine
  • <div><View>, <p><Text>, <button><Pressable>
  • CSS → StyleSheet.create()
  • Pas de SSR

Pour aller plus loin


Cet article a été écrit dans une démarche "learn in public" : je découvre Expo Router en venant de Next.js, et je documente ce que j'apprends. Si tu repères une erreur ou une imprécision, tu peux me contacter via les réseaux sociaux.