Comment mettre à jour vers la version 15

Mise à jour de la version 14 à la version 15

Pour mettre à jour vers Next.js version 15, vous pouvez utiliser le codemod upgrade :

Terminal
npx @next/codemod@canary upgrade latest

Si vous préférez le faire manuellement, assurez-vous d'installer les dernières versions de Next et React :

Terminal
npm i next@latest react@latest react-dom@latest eslint-config-next@latest

Bon à savoir :

  • Si vous voyez un avertissement concernant les dépendances peer, vous devrez peut-être mettre à jour react et react-dom vers les versions suggérées, ou utiliser les flags --force ou --legacy-peer-deps pour ignorer l'avertissement. Ce ne sera plus nécessaire une fois que Next.js 15 et React 19 seront stables.

React 19

  • Les versions minimales de react et react-dom sont désormais 19.
  • useFormState a été remplacé par useActionState. Le hook useFormState est toujours disponible dans React 19, mais il est déprécié et sera supprimé dans une future version. useActionState est recommandé et inclut des propriétés supplémentaires comme la lecture directe de l'état pending. En savoir plus.
  • useFormStatus inclut désormais des clés supplémentaires comme data, method et action. Si vous n'utilisez pas React 19, seule la clé pending est disponible. En savoir plus.
  • Consultez le guide de mise à jour de React 19 pour plus d'informations.

Bon à savoir : Si vous utilisez TypeScript, assurez-vous également de mettre à jour @types/react et @types/react-dom vers leurs dernières versions.

APIs de requête asynchrones (Changement cassant)

Les APIs dynamiques précédemment synchrones qui dépendent d'informations d'exécution sont désormais asynchrones :

Pour faciliter la migration, un codemod est disponible pour automatiser le processus et les APIs peuvent temporairement être accédées de manière synchrone.

cookies

Utilisation asynchrone recommandée

import { cookies } from 'next/headers'

// Avant
const cookieStore = cookies()
const token = cookieStore.get('token')

// Après
const cookieStore = await cookies()
const token = cookieStore.get('token')

Utilisation synchrone temporaire

import { cookies, type UnsafeUnwrappedCookies } from 'next/headers'

// Avant
const cookieStore = cookies()
const token = cookieStore.get('token')

// Après
const cookieStore = cookies() as unknown as UnsafeUnwrappedCookies
// affichera un avertissement en développement
const token = cookieStore.get('token')

headers

Utilisation asynchrone recommandée

import { headers } from 'next/headers'

// Avant
const headersList = headers()
const userAgent = headersList.get('user-agent')

// Après
const headersList = await headers()
const userAgent = headersList.get('user-agent')

Utilisation synchrone temporaire

import { headers, type UnsafeUnwrappedHeaders } from 'next/headers'

// Avant
const headersList = headers()
const userAgent = headersList.get('user-agent')

// Après
const headersList = headers() as unknown as UnsafeUnwrappedHeaders
// affichera un avertissement en développement
const userAgent = headersList.get('user-agent')

draftMode

Utilisation asynchrone recommandée

import { draftMode } from 'next/headers'

// Avant
const { isEnabled } = draftMode()

// Après
const { isEnabled } = await draftMode()

Utilisation synchrone temporaire

import { draftMode, type UnsafeUnwrappedDraftMode } from 'next/headers'

// Avant
const { isEnabled } = draftMode()

// Après
// affichera un avertissement en développement
const { isEnabled } = draftMode() as unknown as UnsafeUnwrappedDraftMode

params & searchParams

Layout asynchrone

// Avant
type Params = { slug: string }

export function generateMetadata({ params }: { params: Params }) {
  const { slug } = params
}

export default async function Layout({
  children,
  params,
}: {
  children: React.ReactNode
  params: Params
}) {
  const { slug } = params
}

// Après
type Params = Promise<{ slug: string }>

export async function generateMetadata({ params }: { params: Params }) {
  const { slug } = await params
}

export default async function Layout({
  children,
  params,
}: {
  children: React.ReactNode
  params: Params
}) {
  const { slug } = await params
}

Layout synchrone

// Avant
type Params = { slug: string }

export default function Layout({
  children,
  params,
}: {
  children: React.ReactNode
  params: Params
}) {
  const { slug } = params
}

// Après
import { use } from 'react'

type Params = Promise<{ slug: string }>

export default function Layout(props: {
  children: React.ReactNode
  params: Params
}) {
  const params = use(props.params)
  const slug = params.slug
}

Page asynchrone

// Avant
type Params = { slug: string }
type SearchParams = { [key: string]: string | string[] | undefined }

export function generateMetadata({
  params,
  searchParams,
}: {
  params: Params
  searchParams: SearchParams
}) {
  const { slug } = params
  const { query } = searchParams
}

export default async function Page({
  params,
  searchParams,
}: {
  params: Params
  searchParams: SearchParams
}) {
  const { slug } = params
  const { query } = searchParams
}

// Après
type Params = Promise<{ slug: string }>
type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>

export async function generateMetadata(props: {
  params: Params
  searchParams: SearchParams
}) {
  const params = await props.params
  const searchParams = await props.searchParams
  const slug = params.slug
  const query = searchParams.query
}

export default async function Page(props: {
  params: Params
  searchParams: SearchParams
}) {
  const params = await props.params
  const searchParams = await props.searchParams
  const slug = params.slug
  const query = searchParams.query
}

Page synchrone

'use client'

// Avant
type Params = { slug: string }
type SearchParams = { [key: string]: string | string[] | undefined }

export default function Page({
  params,
  searchParams,
}: {
  params: Params
  searchParams: SearchParams
}) {
  const { slug } = params
  const { query } = searchParams
}

// Après
import { use } from 'react'

type Params = Promise<{ slug: string }>
type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>

export default function Page(props: {
  params: Params
  searchParams: SearchParams
}) {
  const params = use(props.params)
  const searchParams = use(props.searchParams)
  const slug = params.slug
  const query = searchParams.query
}
// Avant
export default function Page({ params, searchParams }) {
  const { slug } = params
  const { query } = searchParams
}

// Après
import { use } from "react"

export default function Page(props) {
  const params = use(props.params)
  const searchParams = use(props.searchParams)
  const slug = params.slug
  const query = searchParams.query
}

Gestionnaires de route

app/api/route.ts
// Avant
type Params = { slug: string }

export async function GET(request: Request, segmentData: { params: Params }) {
  const params = segmentData.params
  const slug = params.slug
}

// Après
type Params = Promise<{ slug: string }>

export async function GET(request: Request, segmentData: { params: Params }) {
  const params = await segmentData.params
  const slug = params.slug
}
app/api/route.js
// Avant
export async function GET(request, segmentData) {
  const params = segmentData.params
  const slug = params.slug
}

// Après
export async function GET(request, segmentData) {
  const params = await segmentData.params
  const slug = params.slug
}

Configuration runtime (Changement cassant)

La configuration runtime segment configuration supportait auparavant une valeur experimental-edge en plus de edge. Les deux configurations se réfèrent à la même chose, et pour simplifier les options, nous générerons désormais une erreur si experimental-edge est utilisé. Pour corriger cela, mettez à jour votre configuration runtime vers edge. Un codemod est disponible pour le faire automatiquement.

Requêtes fetch

Les requêtes fetch ne sont plus mises en cache par défaut.

Pour activer la mise en cache pour des requêtes fetch spécifiques, vous pouvez passer l'option cache: 'force-cache'.

app/layout.js
export default async function RootLayout() {
  const a = await fetch('https://...') // Non mis en cache
  const b = await fetch('https://...', { cache: 'force-cache' }) // Mis en cache

  // ...
}

Pour activer la mise en cache pour toutes les requêtes fetch dans un layout ou une page, vous pouvez utiliser l'option de configuration de segment export const fetchCache = 'default-cache' segment config option. Si des requêtes fetch individuelles spécifient une option cache, celle-ci sera utilisée à la place.

app/layout.js
// Comme il s'agit du layout racine, toutes les requêtes fetch dans l'application
// qui ne définissent pas leur propre option de cache seront mises en cache.
export const fetchCache = 'default-cache'

export default async function RootLayout() {
  const a = await fetch('https://...') // Mis en cache
  const b = await fetch('https://...', { cache: 'no-store' }) // Non mis en cache

  // ...
}

Gestionnaires de route

Les fonctions GET dans les Gestionnaires de route ne sont plus mises en cache par défaut. Pour activer la mise en cache pour les méthodes GET, vous pouvez utiliser une option de configuration de route comme export const dynamic = 'force-static' dans votre fichier de gestionnaire de route.

app/api/route.js
export const dynamic = 'force-static'

export async function GET() {}

Cache client-side du routeur

Lors de la navigation entre pages via <Link> ou useRouter, les segments de page ne sont plus réutilisés depuis le cache client-side du routeur. Cependant, ils sont toujours réutilisés lors de la navigation arrière et avant du navigateur et pour les layouts partagés.

Pour activer la mise en cache des segments de page, vous pouvez utiliser l'option de configuration staleTimes :

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    staleTimes: {
      dynamic: 30,
      static: 180,
    },
  },
}

module.exports = nextConfig

Les layouts et les états de chargement sont toujours mis en cache et réutilisés lors de la navigation.

next/font

Le package @next/font a été supprimé au profit du next/font intégré. Un codemod est disponible pour renommer automatiquement et en toute sécurité vos imports.

app/layout.js
// Avant
import { Inter } from '@next/font/google'

// Après
import { Inter } from 'next/font/google'

bundlePagesRouterDependencies

experimental.bundlePagesExternals est désormais stable et renommé en bundlePagesRouterDependencies.

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Avant
  experimental: {
    bundlePagesExternals: true,
  },

  // Après
  bundlePagesRouterDependencies: true,
}

module.exports = nextConfig

serverExternalPackages

experimental.serverComponentsExternalPackages est désormais stable et renommé en serverExternalPackages.

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Avant
  experimental: {
    serverComponentsExternalPackages: ['package-name'],
  },

  // Après
  serverExternalPackages: ['package-name'],
}

module.exports = nextConfig

Speed Insights

L'instrumentation automatique pour Speed Insights a été supprimée dans Next.js 15.

Pour continuer à utiliser Speed Insights, suivez le guide de démarrage rapide de Vercel Speed Insights.

Géolocalisation avec NextRequest

Les propriétés geo et ip de NextRequest ont été supprimées car ces valeurs sont désormais fournies par votre hébergeur. Un codemod est disponible pour automatiser cette migration.

Si vous utilisez Vercel, vous pouvez utiliser à la place les fonctions geolocation et ipAddress du package @vercel/functions :

middleware.ts
import { geolocation } from '@vercel/functions'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const { city } = geolocation(request)

  // ...
}
middleware.ts
import { ipAddress } from '@vercel/functions'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const ip = ipAddress(request)

  // ...
}