Migration React : De Create React App vers Next.js 15 en 2026

Brandon Sueur13 min

Create React App (CRA) est officiellement déprécié depuis avril 2023. 78% des projets CRA migrent vers Next.js en 2026.

Ce guide détaille la migration complète CRA → Next.js 15 : architecture, routing, data fetching, performance et SEO. Avec code examples et checklist étape par étape.

Durée migration : 3-7 jours (projet moyen 50 composants)

TL;DR : Pourquoi Migrer CRA → Next.js ?

Critère Create React App Next.js 15 Delta
Maintenance ❌ Déprécié (2023) ✅ Actif (Vercel) -
Performance Lighthouse 60-75 Lighthouse 90-100 +30%
SEO ⚠️ Client-side only ✅ SSR/SSG natif +200% trafic
Bundle size 250-400 KB 80-150 KB -60%
Time to Interactive 3-5s 0.8-1.5s -70%
Developer Experience ⚠️ Config webpack manuelle ✅ Zero-config -
Routing React Router (setup) ✅ File-based natif -
Image optimization ❌ Manual ✅ next/image auto -
TypeScript ⚠️ Config tsconfig ✅ Zero-config -

Gains migration :

  • SEO : +150-300% trafic organique (6 mois)
  • Performance : +35% Lighthouse score
  • Maintenance : -60% temps updates packages
  • Coût hosting : -40% (serverless vs VM)

Prérequis Migration

Versions Minimales

{
  "react": "^19.0.0",
  "react-dom": "^19.0.0",
  "next": "^15.0.0"
}

Checklist Avant Migration

  • Audit codebase (nombre composants, routes, API calls)
  • Identifier dépendances incompatibles Next.js
  • Backup Git (branch migration-nextjs)
  • Tests existants (préserver comportement)
  • Estimate temps migration (3-7 jours moyen)

Étape 1 : Setup Projet Next.js

Installation Fresh Next.js

# Créer nouveau projet Next.js (App Router)
npx create-next-app@latest my-app-nextjs --typescript --tailwind --app

cd my-app-nextjs

# Structure Next.js 15
# my-app-nextjs/
#   ├── app/              # App Router (pages, layouts)
#   ├── components/       # React components (à migrer depuis CRA)
#   ├── public/           # Static assets
#   ├── lib/              # Utilities
#   └── package.json

Copier Code CRA → Next.js

# Copier components depuis CRA
cp -r ../my-app-cra/src/components ./components
cp -r ../my-app-cra/src/lib ./lib
cp -r ../my-app-cra/src/hooks ./hooks
cp -r ../my-app-cra/public/* ./public/

# ⚠️ NE PAS copier :
# - src/index.tsx (remplacé par app/layout.tsx)
# - React Router setup
# - Webpack config

Étape 2 : Migration Routing (React Router → Next.js)

Comprendre les Différences

CRA (React Router) :

// ❌ AVANT : src/App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import Home from './pages/Home'
import About from './pages/About'
import Blog from './pages/Blog'
import BlogPost from './pages/BlogPost'

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/blog" element={<Blog />} />
        <Route path="/blog/:slug" element={<BlogPost />} />
      </Routes>
    </BrowserRouter>
  )
}

Next.js 15 (File-based Routing) :

# ✅ APRÈS : Structure dossiers
app/
  ├── page.tsx              # Route: /
  ├── about/
  │   └── page.tsx          # Route: /about
  ├── blog/
  │   ├── page.tsx          # Route: /blog
  │   └── [slug]/
  │       └── page.tsx      # Route: /blog/:slug (dynamic)
  └── layout.tsx            # Layout global (remplace App.tsx wrapper)

Migration Manuelle Routes

1. Route Simple : /about

// ❌ CRA : src/pages/About.tsx
export default function About() {
  return (
    <div>
      <h1>About Us</h1>
      <p>We are a React agency...</p>
    </div>
  )
}

// ✅ Next.js : app/about/page.tsx
export default function AboutPage() {
  return (
    <div>
      <h1>About Us</h1>
      <p>We are a React agency...</p>
    </div>
  )
}

// ✅ Ajout Metadata SEO (bonus Next.js)
export const metadata = {
  title: 'About Us - My Company',
  description: 'Learn more about our company...',
}

2. Route Dynamique : /blog/:slug

// ❌ CRA : src/pages/BlogPost.tsx
import { useParams } from 'react-router-dom'
import { useEffect, useState } from 'react'

export default function BlogPost() {
  const { slug } = useParams()
  const [post, setPost] = useState(null)

  useEffect(() => {
    fetch(`/api/posts/${slug}`)
      .then((res) => res.json())
      .then(setPost)
  }, [slug])

  if (!post) return <div>Loading...</div>

  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  )
}

// ✅ Next.js : app/blog/[slug]/page.tsx
async function getPost(slug: string) {
  const res = await fetch(`https://api.mysite.com/posts/${slug}`)
  return res.json()
}

export default async function BlogPostPage({ params }: { params: { slug: string } }) {
  // ✅ Server Component : fetch au server-side
  const post = await getPost(params.slug)

  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  )
}

// ✅ Generate static params (SSG)
export async function generateStaticParams() {
  const posts = await fetch('https://api.mysite.com/posts').then((r) => r.json())

  return posts.map((post: any) => ({
    slug: post.slug,
  }))
}

// ✅ Dynamic Metadata
export async function generateMetadata({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug)

  return {
    title: post.title,
    description: post.excerpt,
  }
}

Tableau Conversion Routes

React Router Next.js App Router
<Route path="/" element={<Home />} /> app/page.tsx
<Route path="/about" element={<About />} /> app/about/page.tsx
<Route path="/blog" element={<Blog />} /> app/blog/page.tsx
<Route path="/blog/:slug" element={<Post />} /> app/blog/[slug]/page.tsx
<Route path="/blog/:slug/:id" /> app/blog/[slug]/[id]/page.tsx
<Route path="/dashboard/*" /> app/dashboard/[...slug]/page.tsx

Étape 3 : Migration Data Fetching

Client-Side → Server Components

Principe Next.js 15 : Fetch data côté serveur par défaut (React Server Components)

Pattern 1 : Static Data (SSG)

// ❌ CRA : Client-side fetch
function ProductsPage() {
  const [products, setProducts] = useState([])

  useEffect(() => {
    fetch('/api/products')
      .then((res) => res.json())
      .then(setProducts)
  }, [])

  return <ProductList products={products} />
}

// ✅ Next.js : Server Component (SSG)
async function getProducts() {
  const res = await fetch('https://api.mysite.com/products', {
    next: { revalidate: 3600 }, // ISR : revalidate every hour
  })
  return res.json()
}

export default async function ProductsPage() {
  const products = await getProducts()

  return <ProductList products={products} />
}

Avantages :

  • SEO : HTML pré-rendu avec data
  • Performance : Pas de spinner loading côté client
  • Security : API keys cachées (server-only)

Pattern 2 : Dynamic Data (Client Component si nécessaire)

// ✅ Next.js : Client Component pour interactivité
'use client' // ⚠️ Directive pour Client Component

import { useState, useEffect } from 'react'

export default function LivePrices() {
  const [prices, setPrices] = useState([])

  useEffect(() => {
    // ✅ Fetch client-side pour data temps réel
    const interval = setInterval(async () => {
      const res = await fetch('/api/prices')
      const data = await res.json()
      setPrices(data)
    }, 5000) // Poll every 5s

    return () => clearInterval(interval)
  }, [])

  return <PriceTable prices={prices} />
}

Quand utiliser Client Components :

  • ✅ Event handlers (onClick, onChange, etc.)
  • ✅ State React (useState, useReducer)
  • ✅ Effects (useEffect)
  • ✅ Browser APIs (localStorage, etc.)
  • ✅ Real-time data (WebSocket, polling)

Étape 4 : Migration State Management

Context API (Simple)

CRA :

// ❌ CRA : src/context/AuthContext.tsx
import { createContext, useState } from 'react'

export const AuthContext = createContext(null)

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null)

  return <AuthContext.Provider value={{ user, setUser }}>{children}</AuthContext.Provider>
}

// src/index.tsx
import { AuthProvider } from './context/AuthContext'

root.render(
  <AuthProvider>
    <App />
  </AuthProvider>
)

Next.js :

// ✅ Next.js : context/auth-provider.tsx
'use client' // ⚠️ Context requires Client Component

import { createContext, useState, useContext } from 'react'

const AuthContext = createContext(null)

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState(null)

  return <AuthContext.Provider value={{ user, setUser }}>{children}</AuthContext.Provider>
}

export const useAuth = () => useContext(AuthContext)

// app/layout.tsx (Root Layout)
import { AuthProvider } from '@/context/auth-provider'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <AuthProvider>{children}</AuthProvider>
      </body>
    </html>
  )
}

Zustand (Recommandé pour State Global Complexe)

// ✅ Next.js : store/cart-store.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

type CartStore = {
  items: CartItem[]
  addItem: (item: CartItem) => void
  removeItem: (id: string) => void
}

export const useCartStore = create<CartStore>()(
  persist(
    (set) => ({
      items: [],
      addItem: (item) => set((state) => ({ items: [...state.items, item] })),
      removeItem: (id) =>
        set((state) => ({
          items: state.items.filter((i) => i.id !== id),
        })),
    }),
    { name: 'cart-storage' }
  )
)

// components/cart-button.tsx
;('use client')

import { useCartStore } from '@/store/cart-store'

export function CartButton() {
  const items = useCartStore((state) => state.items)

  return <button>Cart ({items.length})</button>
}

Étape 5 : Migration Styling

CSS Modules (Compatible Next.js)

// ❌ CRA : src/components/Button.tsx
import styles from './Button.module.css'

export function Button({ children }) {
  return <button className={styles.button}>{children}</button>
}

// ✅ Next.js : components/button.tsx (identique!)
import styles from './button.module.css'

export function Button({ children }: { children: React.ReactNode }) {
  return <button className={styles.button}>{children}</button>
}

Aucun changement requis pour CSS Modules !

Tailwind CSS (Recommandé Migration)

// ❌ CRA : CSS-in-JS ou CSS Modules
import styles from './Button.module.css'

// ✅ Next.js : Tailwind (plus moderne)
export function Button({ children }: { children: React.ReactNode }) {
  return (
    <button className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
      {children}
    </button>
  )
}

Étape 6 : Migration Images & Assets

Images Optimization

// ❌ CRA : <img> tag (non-optimized)
export function ProductCard({ product }) {
  return (
    <div>
      <img src={product.image} alt={product.name} width={300} />
      <h3>{product.name}</h3>
    </div>
  )
}

// ✅ Next.js : next/image (auto-optimization)
import Image from 'next/image'

export function ProductCard({ product }: { product: Product }) {
  return (
    <div>
      <Image
        src={product.image}
        alt={product.name}
        width={300}
        height={300}
        quality={90}
        placeholder="blur"
        blurDataURL="/placeholder.jpg"
      />
      <h3>{product.name}</h3>
    </div>
  )
}

Gains :

  • ✅ WebP/AVIF auto-conversion
  • ✅ Lazy loading natif
  • ✅ Responsive images
  • ✅ -60% image weight

Étape 7 : Migration API Routes

CRA Express Backend → Next.js API Routes

// ❌ CRA : Backend Express séparé
// server/routes/products.js
app.get('/api/products', async (req, res) => {
  const products = await db.query('SELECT * FROM products')
  res.json(products)
})

// ✅ Next.js : API Route
// app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'

export async function GET(request: NextRequest) {
  const products = await db.query.products.findMany()

  return NextResponse.json(products)
}

// ✅ Bonus : Server Actions (plus simple!)
// app/actions.ts
'use server'

import { db } from '@/lib/db'

export async function getProducts() {
  return await db.query.products.findMany()
}

// app/products/page.tsx
import { getProducts } from '@/app/actions'

export default async function ProductsPage() {
  const products = await getProducts() // ✅ Direct call (no API endpoint)

  return <ProductList products={products} />
}

Étape 8 : Testing Migration

Tests Unitaires (Jest/Vitest)

// ❌ CRA : Jest config
// jest.config.js
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
}

// ✅ Next.js : Vitest (plus rapide)
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
  },
})

// Test component (identique!)
import { render, screen } from '@testing-library/react'
import { Button } from './button'

test('renders button', () => {
  render(<Button>Click me</Button>)
  expect(screen.getByText('Click me')).toBeInTheDocument()
})

Tests E2E (Playwright)

// tests/e2e/homepage.spec.ts
import { test, expect } from '@playwright/test'

test('homepage loads correctly', async ({ page }) => {
  await page.goto('http://localhost:3000')

  // ✅ Vérifier contenu
  await expect(page.locator('h1')).toContainText('Welcome')

  // ✅ Vérifier navigation
  await page.click('a[href="/about"]')
  await expect(page).toHaveURL(/.*about/)
})

Étape 9 : Deployment

Vercel (Recommandé)

# Install Vercel CLI
npm i -g vercel

# Deploy
vercel

# Production
vercel --prod

Configuration automatique : Zero-config deployment Next.js

Alternative : Docker

# Dockerfile
FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

EXPOSE 3000

CMD ["npm", "start"]

Checklist Migration Complète

✅ Phase 1 : Setup (Jour 1)

  • Créer projet Next.js fresh
  • Copier components/lib/hooks CRA → Next.js
  • Installer dépendances communes
  • Setup Git branch migration-nextjs

✅ Phase 2 : Routing (Jours 2-3)

  • Mapper routes React Router → File-based
  • Créer app/*/page.tsx pour chaque route
  • Migrer routes dynamiques ([slug])
  • Tester navigation complète

✅ Phase 3 : Data Fetching (Jours 3-4)

  • Identifier fetch client-side → Server Components
  • Migrer API calls vers async Server Components
  • Conserver Client Components si useState/useEffect requis
  • Ajouter ISR (revalidate) pour data dynamique

✅ Phase 4 : State & Context (Jour 4)

  • Migrer Context API vers Client Components
  • (Optionnel) Remplacer par Zustand si complexe
  • Tester state management complet

✅ Phase 5 : Styling & Assets (Jour 5)

  • Vérifier CSS Modules fonctionnent
  • (Optionnel) Migrer vers Tailwind
  • Remplacer par
  • Optimiser fonts (next/font)

✅ Phase 6 : API & Backend (Jour 6)

  • Migrer API routes Express → app/api/*/route.ts
  • (Optionnel) Utiliser Server Actions
  • Tester endpoints API

✅ Phase 7 : Testing & QA (Jour 7)

  • Migrer tests unitaires (Jest → Vitest)
  • Ajouter tests E2E (Playwright)
  • Lighthouse audit (target 90+)
  • Cross-browser testing

✅ Phase 8 : Deployment

  • Deploy Vercel staging
  • Test production build
  • Setup domain custom
  • Deploy production

ROI Migration CRA → Next.js

Cas Réel : E-commerce (50 produits)

AVANT (CRA) :

  • Lighthouse : 62/100
  • SEO : Client-side render (poor indexing)
  • Trafic organique : 5k visiteurs/mois
  • Bundle size : 380 KB
  • Time to Interactive : 4.2s

APRÈS (Next.js 15) :

  • Lighthouse : 96/100
  • SEO : SSG (perfect indexing)
  • Trafic organique : 18k visiteurs/mois (+260%)
  • Bundle size : 120 KB (-68%)
  • Time to Interactive : 1.1s (-74%)

Investissement Migration :

  • Temps : 5 jours dev
  • Coût : 3 500€ (TJM 700€)
  • Payback : 3 mois (revenue additionnel trafic SEO)

Conclusion : Migration Simple & ROI Fort

Migration CRA → Next.js 15 en 2026 :

  • Durée : 3-7 jours (projet moyen)
  • Complexité : Moyenne (si React déjà maîtrisé)
  • ROI : +150-300% SEO traffic, +35% performance
  • Maintenance : -60% temps (Next.js zero-config)

Notre recommandation Hulli Studio :

  • Projets CRA legacy : Migrer NOW (CRA déprécié)
  • Nouveaux projets : Next.js obligatoire (jamais CRA)
  • Budget migration : 3k€ - 8k€ (50-100 composants)

Besoin d'aide migration CRA → Next.js ? Contactez Hulli Studio pour audit gratuit + estimation.


Articles connexes :