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
:
npx @next/codemod@canary upgrade latest
Si prefieres hacerlo manualmente, asegúrate de instalar las últimas versiones de Next y React:
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
yreact-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
yreact-dom
son ahora la 19. useFormState
ha sido reemplazado poruseActionState
. El hookuseFormState
sigue disponible en React 19, pero está obsoleto y se eliminará en una futura versión. Se recomienda usaruseActionState
, que incluye propiedades adicionales como leer directamente el estadopending
. Más información.useFormStatus
ahora incluye claves adicionales comodata
,method
yaction
. Si no estás usando React 19, solo estará disponible la clavepending
. 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:
cookies
headers
draftMode
params
enlayout.js
,page.js
,route.js
,default.js
,opengraph-image
,twitter-image
,icon
, yapple-icon
.searchParams
enpage.js
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')
import { cookies } from 'next/headers'
// Antes
const cookieStore = cookies()
const token = cookieStore.get('token')
// Después
const cookieStore = cookies()
// 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')
import { headers } from 'next/headers'
// Antes
const headersList = headers()
const userAgent = headersList.get('user-agent')
// Después
const headersList = headers()
// 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
import { draftMode } from 'next/headers'
// Antes
const { isEnabled } = draftMode()
// Después
// mostrará una advertencia en desarrollo
const { isEnabled } = draftMode()
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
}
// Antes
export function generateMetadata({ params }) {
const { slug } = params
}
export default async function Layout({ children, params }) {
const { slug } = params
}
// Después
export async function generateMetadata({ params }) {
const { slug } = await params
}
export default async function Layout({ children, 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
}
// Antes
export default function Layout({ children, params }) {
const { slug } = params
}
// Después
import { use } from 'react'
export default async function Layout(props) {
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
}
// Antes
export function generateMetadata({ params, searchParams }) {
const { slug } = params
const { query } = searchParams
}
export default function Page({ params, searchParams }) {
const { slug } = params
const { query } = searchParams
}
// Después
export async function generateMetadata(props) {
const params = await props.params
const searchParams = await props.searchParams
const slug = params.slug
const query = searchParams.query
}
export async function Page(props) {
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
// 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
}
// 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'
.
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.
// 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.
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
:
/** @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.
// 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
.
/** @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
.
/** @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
:
import { geolocation } from '@vercel/functions'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const { city } = geolocation(request)
// ...
}
import { ipAddress } from '@vercel/functions'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const ip = ipAddress(request)
// ...
}