Migration React : De Create React App vers Next.js 15 en 2026
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 :