Cómo actualizar a la versión 15

Actualización de la versión 14 a la 15

Para actualizar a Next.js versión 15, puedes usar el codemod upgrade:

Terminal
npx @next/codemod@canary upgrade latest

Si prefieres hacerlo manualmente, asegúrate de instalar las últimas versiones de Next y React:

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

Nota importante:

  • Si ves una advertencia de dependencias peer, puede que necesites actualizar react y react-dom a las versiones sugeridas, o usar los flags --force o --legacy-peer-deps para ignorar la advertencia. Esto no será necesario una vez que tanto Next.js 15 como React 19 sean estables.

React 19

  • Las versiones mínimas de react y react-dom son ahora la 19.
  • useFormState ha sido reemplazado por useActionState. El hook useFormState sigue disponible en React 19, pero está obsoleto y se eliminará en una futura versión. Se recomienda usar useActionState, que incluye propiedades adicionales como leer directamente el estado pending. Más información.
  • useFormStatus ahora incluye claves adicionales como data, method y action. Si no estás usando React 19, solo estará disponible la clave pending. Más información.
  • Lee más en la guía de actualización a React 19.

Nota importante: Si estás usando TypeScript, asegúrate de actualizar también @types/react y @types/react-dom a sus últimas versiones.

APIs de solicitud asíncrona (Cambio importante)

Las APIs dinámicas que antes eran síncronas y dependían de información en tiempo de ejecución ahora son asíncronas:

Para facilitar la migración, hay disponible un codemod que automatiza el proceso y las APIs pueden accederse temporalmente de forma síncrona.

cookies

Uso asíncrono recomendado

import { cookies } from 'next/headers'

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

// Después
const cookieStore = await cookies()
const token = cookieStore.get('token')

Uso síncrono temporal

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

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

// Después
const cookieStore = cookies() as unknown as UnsafeUnwrappedCookies
// mostrará una advertencia en desarrollo
const token = cookieStore.get('token')

headers

Uso asíncrono recomendado

import { headers } from 'next/headers'

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

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

Uso síncrono temporal

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

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

// Después
const headersList = headers() as unknown as UnsafeUnwrappedHeaders
// mostrará una advertencia en desarrollo
const userAgent = headersList.get('user-agent')

draftMode

Uso asíncrono recomendado

import { draftMode } from 'next/headers'

// Antes
const { isEnabled } = draftMode()

// Después
const { isEnabled } = await draftMode()

Uso síncrono temporal

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

// Antes
const { isEnabled } = draftMode()

// Después
// mostrará una advertencia en desarrollo
const { isEnabled } = draftMode() as unknown as UnsafeUnwrappedDraftMode

params y searchParams

Layout asíncrono

// Antes
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
}

// Despué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 síncrono

// Antes
type Params = { slug: string }

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

// Despué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
}

Página asíncrona

// Antes
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
}

// Despué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
}

Página síncrona

'use client'

// Antes
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
}

// Despué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
}
// Antes
export default function Page({ params, searchParams }) {
  const { slug } = params
  const { query } = searchParams
}

// Despué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
}

Manejadores de ruta

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

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

// Despué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
// Antes
export async function GET(request, segmentData) {
  const params = segmentData.params
  const slug = params.slug
}

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

Configuración runtime (Cambio importante)

La configuración runtime de segmento de ruta admitía anteriormente un valor experimental-edge además de edge. Ambas configuraciones se refieren a lo mismo, y para simplificar las opciones, ahora mostraremos un error si se usa experimental-edge. Para solucionarlo, actualiza tu configuración runtime a edge. Hay disponible un codemod para hacer esto automáticamente.

Solicitudes fetch

Las solicitudes fetch ya no se almacenan en caché por defecto.

Para activar el almacenamiento en caché para solicitudes fetch específicas, puedes pasar la opción cache: 'force-cache'.

app/layout.js
export default async function RootLayout() {
  const a = await fetch('https://...') // No se almacena en caché
  const b = await fetch('https://...', { cache: 'force-cache' }) // Almacenado en caché

  // ...
}

Para activar el almacenamiento en caché para todas las solicitudes fetch en un layout o página, puedes usar la opción de configuración de segmento export const fetchCache = 'default-cache' segment config option. Si solicitudes fetch individuales especifican una opción cache, se usará esa en su lugar.

app/layout.js
// Como este es el layout raíz, todas las solicitudes fetch en la aplicación
// que no establezcan su propia opción cache se almacenarán en caché.
export const fetchCache = 'default-cache'

export default async function RootLayout() {
  const a = await fetch('https://...') // Almacenado en caché
  const b = await fetch('https://...', { cache: 'no-store' }) // No almacenado en caché

  // ...
}

Manejadores de ruta

Las funciones GET en Manejadores de ruta ya no se almacenan en caché por defecto. Para activar el almacenamiento en caché para métodos GET, puedes usar una opción de configuración de ruta como export const dynamic = 'force-static' en tu archivo de Manejador de ruta.

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

export async function GET() {}

Caché del enrutador del lado del cliente

Al navegar entre páginas mediante <Link> o useRouter, los segmentos de página ya no se reutilizan desde la caché del enrutador del lado del cliente. Sin embargo, todavía se reutilizan durante la navegación hacia atrás y adelante del navegador y para diseños compartidos.

Para activar el almacenamiento en caché de segmentos de página, puedes usar la opción de configuración staleTimes:

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

module.exports = nextConfig

Los diseños y estados de carga todavía se almacenan en caché y se reutilizan en la navegación.

next/font

El paquete @next/font ha sido eliminado en favor del next/font incorporado. Hay disponible un codemod para renombrar tus importaciones de forma segura y automática.

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

// Después
import { Inter } from 'next/font/google'

bundlePagesRouterDependencies

experimental.bundlePagesExternals ahora es estable y se ha renombrado a bundlePagesRouterDependencies.

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

  // Después
  bundlePagesRouterDependencies: true,
}

module.exports = nextConfig

serverExternalPackages

experimental.serverComponentsExternalPackages ahora es estable y se ha renombrado a serverExternalPackages.

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

  // Después
  serverExternalPackages: ['package-name'],
}

module.exports = nextConfig

Speed Insights

La instrumentación automática para Speed Insights se eliminó en Next.js 15.

Para seguir usando Speed Insights, sigue la guía rápida de Vercel Speed Insights.

Geolocalización en NextRequest

Las propiedades geo e ip en NextRequest han sido eliminadas ya que estos valores son proporcionados por su proveedor de alojamiento. Existe un codemod disponible para automatizar esta migración.

Si está utilizando Vercel, puede usar alternativamente las funciones geolocation e ipAddress de @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)

  // ...
}