Changer le thème

Next.js Pages Router vers App Router : guide pratique et pièges à éviter

Easton editorial illustration: island architecture model

Le directeur technique pose la question en réunion : « Notre projet Next.js 12, on peut passer à la 14 ? »

Je fixe l’écran — ce vieux projet tourne depuis deux ans. La dernière montée vers React 17 nous avait coûté une semaine de bugs, le support client débordé.

Cette fois, c’est un peu différent. Le soir, je feuillette la doc officielle : Server Components, layouts imbriqués, meilleures perfs… ça donne envie. Puis la page migration : tableaux d’API, getServerSideProps devient quoi, _app.js se découpe comment… la migraine.

Pire : la « migration progressive » recommandée sonne bien, mais en pratique, basculer entre /pages et /app affiche un spinner — l’expérience se dégrade.

Deux semaines de galère, de threads communautaires et d’essais. Voici ce qui en est sorti :

  • Comment juger si votre projet vaut le coup
  • Deux stratégies, avantages et inconvénients réels (pas la théorie officielle)
  • getServerSideProps : étapes et exemples
  • 7 pièges que j’ai creusés moi-même

Si vous hésitez à upgrader ou si la migration coince, ce guide devrait vous faire gagner du temps.

Pourquoi migrer ? Faites le calcul

Un rappel froid : toutes les migrations ne valent pas le coup.

Un ami m’a demandé s’il fallait migrer une landing promo bientôt supprimée. Réponse : non — code jetable dans six mois, inutile d’y passer du temps.

Quand migrer ? Quelques critères :

Besoin de layouts imbriqués

C’était notre déclencheur. Notre back-office SaaS : sidebar + header + contenu sur trois niveaux. En Pages Router, chaque navigation re-rendait toute la sidebar.

L’utilisateur sentait l’interface clignoter — pas le réseau, le layout.

Les layouts imbriqués d’App Router règlent ça. WorkOS rapporte une « connexion nettement améliorée, sans loading ni jitter » — nos tests confirment : seul le contenu bouge, la nav reste fixe.

Marge d’optimisation perf

Premier affichage > 3 s ? App Router peut aider.

Notre liste produits utilisait getServerSideProps : chaque refresh attendait le HTML serveur. En Server Components, données streamées côté serveur — 3,2 s → 1,8 s.

Attention : pas toutes les pages gagnent. Un éditeur canvas côté client ? Quasi aucun gain, parfois un peu plus lent.

Projet sur le long terme

Maintenance 3 ans ou plus : migrer tôt. Vercel oriente les nouveautés vers App Router ; Pages Router est en mode maintenance.

Pas envie de refaire ça dans deux ans avec des API encore différentes.

Quand ne pas migrer

  • Projet bientôt arrêté — inutile
  • Petit site statique (≤ 5 pages) — ROI faible
  • Équipe peu à l’aise React 18 — Suspense et Server Components d’abord
  • Beaucoup de vieilles libs — incompatibilités en cascade

En bref : ne migrez pas pour la mode. Quel problème concret résolvez-vous ? Si la réponse est « aucun, curiosité », laissez tomber.

Notre calcul : deux semaines-homme pour une UX meilleure et moins de dette sur trois ans — rentable. Et vous ?

Choisir entre deux stratégies

La doc recommande la migration progressive — page par page, en douceur.

En pratique, un piège majeur.

Le piège de la migration progressive

Scénario : l’accueil est dans /app, la fiche produit encore dans /pages. Clic depuis l’accueil → écran blanc, spinner… puis le contenu.

Pourquoi ? Saut App Router → Pages Router = deux apps distinctes, rechargement complet du bundle JS. Retour en 2010.

WorkOS le dit aussi : « naviguer entre routeurs, c’est comme changer d’application ». Ils voulaient du progressif, ils ont abandonné.

Pas totalement impossible pourtant.

Cas adaptés :

  • Pages peu couplées (blog)
  • Migration par module entier (tout le compte utilisateur, puis le catalogue)
  • Loading acceptable entre versions

Un blog tech l’a fait — correct. SaaS ou e-commerce : oubliez.

Le schéma zéro downtime de WorkOS

Grands projets ? WorkOS propose une astuce.

Recréer toutes les pages sous /app/new, bascule par paramètre de requête :

// next.config.js
module.exports = {
  async rewrites() {
    return [
      {
        source: '/:path*',
        destination: '/new/:path*',
        has: [
          {
            type: 'query',
            key: 'new',
            value: 'true',
          },
        ],
      },
    ]
  },
}

/dashboard reste l’ancienne version ; ?new=true montre la nouvelle.

Test, produit, design en prod sans impact utilisateur. Une fois validé : /app/new → /app, suppression de /pages et des rewrites.

C’est notre approche. Zéro bug utilisateur en prod — une semaine de tests sur données réelles avant bascule.

Étapes :

  1. Monter Next.js 14 — /pages intact pour l’instant
  2. Hooks de routing — next/router → next/navigation, compatibilité des deux routeurs
  3. Créer /app/new — structure des pages
  4. Réutiliser les composants — import depuis /pages
  5. Configurer rewrites — bascule ?new=true
  6. Tests internes + canary — équipe sur la nouvelle version
  7. Mise en prod — /app/new → /app, nettoyage

De l’étape 1 à 7 : 10 jours ouvrés. 6 jours de pages, 3 de bugs, 1 de déploiement.

Mon conseil

  • < 10 pages, indépendantes → progressive
  • 10 pages, UX exigeante → zéro downtime

  • Nouveau projet → App Router direct

Évitez de migrer en parallèle du développement feature — deux styles de code, c’est pénible. Deux semaines dédiées, ou attendez.

getServerSideProps en pratique

La question la plus fréquente : « getServerSideProps n’existe plus, comment fetcher ? »

App Router simplifie — il faut surtout changer de mentalité.

De la séparation à la fusion

Pages Router : données (getServerSideProps) et UI séparées ; Next.js appelle la fonction serveur et passe les props.

App Router : la page est une fonction async qui fetch directement :

// ❌ Ancien : pages/project/[id].tsx
export async function getServerSideProps(context) {
  const { id } = context.params
  const res = await fetch(`https://api.example.com/projects/${id}`)
  const project = await res.json()

  return {
    props: { project }
  }
}

export default function ProjectPage({ project }) {
  return <h1>{project.title}</h1>
}
// ✅ Nouveau : app/project/[id]/page.tsx
export default async function ProjectPage({ params }) {
  const { id } = params
  const res = await fetch(`https://api.example.com/projects/${id}`, {
    cache: 'no-store' // crucial — équivalent getServerSideProps
  })
  const project = await res.json()

  return <h1>{project.title}</h1>
}

Plus simple ? Deux gros pièges derrière.

Piège 1 : mauvaise config cache

Par défaut, fetch en App Router est mis en cache (comme getStaticProps), pas à chaque requête.

J’ai migré une page prix sans y penser — prix figés, plaintes clients. Une demi-journée pour trouver le cache.

Table de correspondance :

  • getServerSidePropscache: 'no-store'
  • getStaticPropscache: 'force-cache' (défaut)
  • getStaticProps + revalidatenext: { revalidate: 60 }

Piège 2 : état client

Pages avec getServerSideProps + filtres, tri… En App Router, un async Server Component ne peut pas utiliser useState, useEffect.

Solution : découper.

// app/products/page.tsx (Server Component)
export default async function ProductsPage() {
  const products = await fetchProducts()

  return <ProductList initialData={products} />
}
// components/ProductList.tsx (Client Component)
'use client'

import { useState } from 'react'

export function ProductList({ initialData }) {
  const [products, setProducts] = useState(initialData)
  const [filter, setFilter] = useState('')

  const filtered = products.filter(p => p.name.includes(filter))

  return (
    <div>
      <input value={filter} onChange={e => setFilter(e.target.value)} />
      {filtered.map(p => <ProductCard key={p.id} product={p} />)}
    </div>
  )
}

Serveur pour les données, client pour l’interaction.

Ne surchargez pas “use client” — toute la page en client, c’est perdre l’intérêt des Server Components.

Étapes de migration

Étape 1 : découper
Dans /pages, séparer affichage pur et composants avec état. Tester.

Étape 2 : déplacer vers /app

  • Affichage pur → app/[route]/page.tsx, async, fetch dedans
  • État → fichier séparé avec “use client”
  • Supprimer getServerSideProps

Rollback facile si problème.

Détail : cookies et headers

Ancien context.req.cookies :

import { cookies } from 'next/headers'

export default async function Page() {
  const cookieStore = cookies()
  const token = cookieStore.get('auth-token')

  // token → requête utilisateur...
}

Idem pour headers(), redirect() depuis next/headers ou next/navigation.

7 pièges courants et solutions

Les sept suivants, je les ai tous creusés — au moins une heure de debug chacun.

Piège 1 : erreurs serveur avalées

Symptôme : page blanche ou skeleton, pas d’erreur visible.

Mon cas : API cassée, écran vide, console vide. Pas de error.tsx → Next.js avale l’exception, affiche le fallback Suspense.

Solution : error.tsx par segment :

// app/dashboard/error.tsx
'use client'

export default function Error({ error, reset }) {
  return (
    <div>
      <h2>Erreur : {error.message}</h2>
      <button onClick={reset}>Réessayer</button>
    </div>
  )
}

Dev : stack détaillée. Prod : message utilisateur.

Piège 2 : useRouter ne fonctionne plus

Symptôme : useRouter().push() ne navigue pas ou méthode absente.

Cause : next/router ≠ next/navigation.

// ❌ Erreur
import { useRouter } from 'next/navigation'

const router = useRouter()
router.push('/dashboard') // push absent !

En fait push existe parfois, mais le comportement diffère — préférez Link :

// ✅ Mieux
import { useRouter, usePathname, useSearchParams } from 'next/navigation'

const router = useRouter()
router.push('/dashboard')

import Link from 'next/link'
<Link href="/dashboard">Aller au dashboard</Link>

Table (collée sur mon écran pendant la migration) :

Pages RouterApp Router
useRouter().push(url)useRouter().push(url) (existe, déconseillé)
useRouter().pathnameusePathname()
useRouter().queryuseSearchParams()
useRouter().asPathusePathname() + useSearchParams()

Piège 3 : import dynamique cassé

Symptôme : next/dynamic ne rend rien — « You’re importing a component that needs useState… »

Cause : Server Component côté serveur ; libs client-only (graphiques) plantent.

Mon page ECharts :

// ❌ Erreur
import dynamic from 'next/dynamic'

const Chart = dynamic(() => import('./Chart'), { ssr: false })

export default function Page() {
  return <Chart data={data} />
}

Chart veut window, absent côté serveur.

Solution : “use client” sur la page, ou composant client dédié :

// app/charts/page.tsx
import { ClientChart } from './ClientChart'

export default function Page() {
  return <ClientChart />
}
// app/charts/ClientChart.tsx
'use client'

import dynamic from 'next/dynamic'

const Chart = dynamic(() => import('./Chart'), { ssr: false })

export function ClientChart() {
  return <Chart data={data} />
}

Piège 4 : scintillement à la navigation

Symptôme : header et sidebar clignotent à chaque lien.

Cause : pas de layout, ou mal configuré. J’avais la nav dans chaque page.tsx — évidemment ça flash.

Correct :

// app/layout.tsx (racine)
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Header />
        {children}
      </body>
    </html>
  )
}
// app/dashboard/layout.tsx
export default function DashboardLayout({ children }) {
  return (
    <div className="flex">
      <Sidebar />
      <main>{children}</main>
    </div>
  )
}

Entre /dashboard/analytics et /dashboard/settings, seul <main> change.

Piège 5 : 404 personnalisée ignorée

Symptôme : 404 par défaut Next.js au lieu de la vôtre.

Cause : /pages/404.js encore présent — bloque app/not-found.tsx.

Solution : supprimer /pages/404.js et /pages/500.js :

// app/not-found.tsx
export default function NotFound() {
  return <h1>Page introuvable</h1>
}

Déclencher depuis une page :

import { notFound } from 'next/navigation'

export default async function Page({ params }) {
  const data = await fetchData(params.id)

  if (!data) {
    notFound()
  }

  return <div>{data.title}</div>
}

Piège 6 : dev server de plus en plus lent

Symptôme : HMR à 10 s, puis crash.

Pas de solution parfaite de mon côté.

Problème connu Next.js 14. FlightControl : « perf dev server si mauvaise qu’on renoncerait aux nouveautés » — redémarrage toutes les 20 minutes.

Contournements :

  • Redémarrer le dev server (~15 min, rappel)
  • next dev --turbo (Turbopack, parfois instable)
  • Moins de Server Components là où le client suffit

Next.js 15 aurait amélioré — pas encore testé chez nous.

Piège 7 : libs tierces incompatibles

Symptôme : Framer Motion, Lottie — window ou document introuvable.

Cause : libs purement client dans un Server Component.

Mes animations Framer Motion : tout cassé après migration.

Solutions :

  1. “use client” sur le composant concerné
  2. Vérifier une version React 18 compatible
  3. Changer de lib si nécessaire

Libs souvent client-only :

  • Framer Motion (animations de sortie — issues ouvertes)
  • swiper, slick-carousel
  • ECharts, Chart.js
  • react-dnd, dnd-kit

Projets lourds en animations : lire les issues GitHub avant de migrer.

Optimisations post-migration

La migration n’est pas la ligne d’arrivée.

Réduire le JavaScript client

Atout des Server Components.

Notre liste produits : 120 Ko gzip en React pur. Après migration, affichage en Server Component, filtres en Client — 45 Ko.

Vérifier :

npm run build

Pages (Static)/(SSR) vs ○ (Client Component). Tout en ○ = trop de “use client”.

Conseils :

  • Texte et images → Server Component
  • Formulaires, boutons → Client Component
  • “use client” sur les sous-composants, pas toute la page

Cache raisonné

Plus fin qu’en Pages Router :

// Temps réel
fetch(url, { cache: 'no-store' })

// Revalidation 60 s
fetch(url, { next: { revalidate: 60 } })

// Statique
fetch(url, { cache: 'force-cache' })

Liste produits : revalidate 60 s — données fraîches, −60 % d’appels API.

Monitoring perf

Comparer :

  • FCP — premier contenu visible
  • TTI — page interactive
  • CLS — stabilité visuelle

Vercel Analytics : FCP 3,2 s → 1,8 s, TTI 5,1 s → 3,3 s. L’éditeur canvas : quasi identique.

Éviter la sur-optimisation

Ne découpez pas un formulaire en 20 micro-composants pour le KPI Server Component.

Règle : besoin de useState/useEffect → “use client”, point. Les Server Components sont un outil, pas un objectif.

Conclusion

Migrer n’est pas une mode — c’est résoudre un problème : layouts imbriqués, moins de JS client, maintenance longue.

Stratégie : petit projet progressif, grand projet zéro downtime. Pas de migration en parallèle du feature dev.

Les pièges sont normaux — les sept ci-dessus ne sont que la surface. GitHub issues d’abord.

Pas de sur-optimisation — lisibilité et vélocité d’équipe > taille de bundle.

Conseil : pilotez 1–2 pages, documentez, puis généralisez. Chez nous : 3 jours la première, 5 jours pour les dix suivantes.

App Router a des défauts (surtout le dev server), mais la direction est bonne. L’écosystème comblera les trous.

Un blocage en migration ? Échangez en commentaire — j’ai peut-être le même sous les pieds.


Ressources :

Bonne migration !

Processus complet de migration Pages Router vers App Router

Étapes de l'évaluation au déploiement, avec choix de stratégie et résolution des problèmes courants

⏱️ Estimated time: 80 hr

  1. 1

    Step 1: Évaluer si la migration en vaut la peine

    Critères :
    • Layouts imbriqués : plusieurs niveaux sans re-render à chaque navigation
    • Optimisation perf : premier affichage > 3 s avec marge d'amélioration
    • Maintenance long terme : projet maintenu 3 ans ou plus — migrer tôt pour en profiter

    Ne pas migrer si :
    • Projet bientôt arrêté
    • Petit site statique (≤ 5 pages)
    • Équipe peu à l'aise avec React 18
    • Forte dépendance à des libs tierces anciennes
  2. 2

    Step 2: Choisir la stratégie de migration

    Selon la taille du projet :

    Petit projet (< 10 pages, pages indépendantes) → migration progressive :
    • Page par page
    • Loading acceptable entre /pages et /app
    • Adapté aux blogs, faible couplage

    Grand projet (> 10 pages, exigences UX) → zéro downtime :
    • Reconstruire toutes les pages sous /app/new
    • rewrites + paramètre de requête pour basculer
    • Mise en prod après tests internes
  3. 3

    Step 3: Migrer getServerSideProps

    Étapes :
    1. Transformer la page en fonction async
    2. fetch des données directement dans le composant
    3. Options cache :
    • getServerSideProps → cache: 'no-store'
    • getStaticProps → cache: 'force-cache'
    • getStaticProps + revalidate → next: { revalidate: 60 }

    4. Interactions client dans un Client Component :
    • Server Component pour les données
    • Client Component pour useState, useEffect, etc.
  4. 4

    Step 4: Routes et navigation

    Mettre à jour le code routing :
    • next/router → next/navigation
    • useRouter().pathname → usePathname()
    • useRouter().query → useSearchParams()
    • Préférer Link à router.push()

    Note : useRouter de next/navigation se comporte différemment — privilégiez Link
  5. 5

    Step 5: Configurer les layouts

    Layouts imbriqués pour éviter le scintillement :
    • app/layout.tsx racine (Header, Footer)
    • Sous-layouts par zone (ex. app/dashboard/layout.tsx)
    • Chaque layout n'ajoute que l'UI propre à son niveau
    • Héritage automatique, pas de re-render parent au changement de page
  6. 6

    Step 6: Erreurs et 404

    Erreurs :
    • error.tsx pour capturer et afficher un message clair
    • Marquer error avec 'use client'

    404 :
    • Supprimer /pages/404.js
    • Créer app/not-found.tsx
    • Appeler notFound() dans page.tsx si besoin
  7. 7

    Step 7: Tests et optimisation

    Tests :
    • Toutes les routes
    • Récupération des données
    • Interactions client
    • Pas de scintillement entre layouts

    Perf :
    • Réduire les 'use client' inutiles
    • Stratégie de cache adaptée
    • Surveiller FCP, TTI, CLS
    • Comparer avant/après migration

FAQ

Quelle différence entre migration progressive et zéro downtime ?
Progressive : page par page, adaptée aux petits projets ; bascule /pages ↔ /app avec état loading.

Zéro downtime : tout reconstruit sous /app/new, bascule par paramètre de requête, prod après validation — grands projets et UX exigeante.
Comment récupérer les données après migration de getServerSideProps ?
Page en fonction async, fetch dans le composant.

Cache :
• getServerSideProps → cache: 'no-store'
• getStaticProps → cache: 'force-cache'

Interactions client : Server Component (données) + Client Component (UI interactive).
Pourquoi scintille-t-on au changement de page après migration ?
Souvent un mauvais usage des layouts. Créez app/layout.tsx et des sous-layouts par zone : seule la zone contenu se met à jour, barre de nav et sidebar restent stables.
Comment utiliser useRouter dans App Router ?
next/navigation, pas next/router. pathname → usePathname(), query → useSearchParams(). Préférez Link à router.push().
Libs tierces incompatibles après migration ?
Marquez 'use client' sur les composants concernés.

Exemples : Framer Motion, ECharts, Chart.js, carrousels.

Vérifiez les issues GitHub avant de migrer.
Dev server de plus en plus lent ?
Problème connu de Next.js 14.

Contournements :
• Redémarrer le dev server régulièrement (~15 min)
• next dev --turbo (Turbopack)
• Moins de Server Components inutiles

Next.js 15 aurait amélioré ce point.
Combien de temps dure une migration ?
Selon la taille :
• Petit projet (< 10 pages) : 3 à 5 jours
• Grand projet : 2 à 3 semaines

Pilotez 1–2 pages d'abord. Chez nous : 3 jours pour la première, 5 jours pour les 10 suivantes.

10 min de lecture · Publié le: 18 déc. 2025 · Mis à jour le: 27 juil. 2026

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog