كيفية الترقية إلى الإصدار 15

الترقية من الإصدار 14 إلى 15

للتحديث إلى إصدار Next.js 15، يمكنك استخدام أداة upgrade التلقائية:

Terminal
npx @next/codemod@canary upgrade latest

إذا كنت تفضل القيام بذلك يدويًا، تأكد من تثبيت أحدث إصدارات Next و React:

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

معلومة مفيدة:

  • إذا رأيت تحذيرًا حول تبعيات الأقران، قد تحتاج إلى تحديث react و react-dom إلى الإصدارات المقترحة، أو يمكنك استخدام علم --force أو --legacy-peer-deps لتجاهل التحذير. لن يكون هذا ضروريًا بمجرد استقرار كل من Next.js 15 و React 19.

React 19

  • الحد الأدنى لإصدارات react و react-dom أصبح الآن 19.
  • تم استبدال useFormState بـ useActionState. لا يزال خطاف useFormState متاحًا في React 19، ولكنه مهمل وسيتم إزالته في إصدار مستقبلي. يُوصى باستخدام useActionState لأنه يتضمن خصائص إضافية مثل قراءة حالة pending مباشرة. معرفة المزيد.
  • useFormStatus يتضمن الآن مفاتيح إضافية مثل data و method و action. إذا كنت لا تستخدم React 19، فإن مفتاح pending فقط هو المتاح. معرفة المزيد.
  • اقرأ المزيد في دليل ترقية React 19.

معلومة مفيدة: إذا كنت تستخدم TypeScript، تأكد من تحديث @types/react و @types/react-dom إلى أحدث إصداراتهم.

واجهات برمجة التطبيقات غير المتزامنة (تغيير كاسر)

أصبحت واجهات برمجة التطبيقات الديناميكية التي كانت متزامنة وتعتمد على معلومات وقت التشغيل الآن غير متزامنة:

لتسهيل عملية الترحيل، تتوفر أداة تلقائية لأتمتة العملية ويمكن الوصول إلى واجهات برمجة التطبيقات مؤقتًا بشكل متزامن.

cookies

الاستخدام الموصى به غير المتزامن

import { cookies } from 'next/headers'

// قبل
const cookieStore = cookies()
const token = cookieStore.get('token')

// بعد
const cookieStore = await cookies()
const token = cookieStore.get('token')

الاستخدام المتزامن المؤقت

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

// قبل
const cookieStore = cookies()
const token = cookieStore.get('token')

// بعد
const cookieStore = cookies() as unknown as UnsafeUnwrappedCookies
// سيسجل تحذيرًا في وضع التطوير
const token = cookieStore.get('token')
import { cookies } from 'next/headers'

// قبل
const cookieStore = cookies()
const token = cookieStore.get('token')

// بعد
const cookieStore = cookies()
// سيسجل تحذيرًا في وضع التطوير
const token = cookieStore.get('token')

headers

الاستخدام الموصى به غير المتزامن

import { headers } from 'next/headers'

// قبل
const headersList = headers()
const userAgent = headersList.get('user-agent')

// بعد
const headersList = await headers()
const userAgent = headersList.get('user-agent')

الاستخدام المتزامن المؤقت

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

// قبل
const headersList = headers()
const userAgent = headersList.get('user-agent')

// بعد
const headersList = headers() as unknown as UnsafeUnwrappedHeaders
// سيسجل تحذيرًا في وضع التطوير
const userAgent = headersList.get('user-agent')
import { headers } from 'next/headers'

// قبل
const headersList = headers()
const userAgent = headersList.get('user-agent')

// بعد
const headersList = headers()
// سيسجل تحذيرًا في وضع التطوير
const userAgent = headersList.get('user-agent')

draftMode

الاستخدام الموصى به غير المتزامن

import { draftMode } from 'next/headers'

// قبل
const { isEnabled } = draftMode()

// بعد
const { isEnabled } = await draftMode()

الاستخدام المتزامن المؤقت

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

// قبل
const { isEnabled } = draftMode()

// بعد
// سيسجل تحذيرًا في وضع التطوير
const { isEnabled } = draftMode() as unknown as UnsafeUnwrappedDraftMode
import { draftMode } from 'next/headers'

// قبل
const { isEnabled } = draftMode()

// بعد
// سيسجل تحذيرًا في وضع التطوير
const { isEnabled } = draftMode()

params و searchParams

التخطيط غير المتزامن

// قبل
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
}

// بعد
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
}
// قبل
export function generateMetadata({ params }) {
  const { slug } = params
}

export default async function Layout({ children, params }) {
  const { slug } = params
}

// بعد
export async function generateMetadata({ params }) {
  const { slug } = await params
}

export default async function Layout({ children, params }) {
  const { slug } = await params
}

التخطيط المتزامن

// قبل
type Params = { slug: string }

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

// بعد
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
}
// قبل
export default function Layout({ children, params }) {
  const { slug } = params
}

// بعد
import { use } from 'react'
export default async function Layout(props) {
  const params = use(props.params)
  const slug = params.slug
}

الصفحة غير المتزامنة

// قبل
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
}

// بعد
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
}
// قبل
export function generateMetadata({ params, searchParams }) {
  const { slug } = params
  const { query } = searchParams
}

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

// بعد
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
}

الصفحة المتزامنة

'use client'

// قبل
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
}

// بعد
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
}
// قبل
export default function Page({ params, searchParams }) {
  const { slug } = params
  const { query } = searchParams
}

// بعد
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
}

معالجات المسار

app/api/route.ts
// قبل
type Params = { slug: string }

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

// بعد
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
// قبل
export async function GET(request, segmentData) {
  const params = segmentData.params
  const slug = params.slug
}

// بعد
export async function GET(request, segmentData) {
  const params = await segmentData.params
  const slug = params.slug
}

تكوين runtime (تغيير كاسر)

كان تكوين runtime لقطاع المسار يدعم سابقًا قيمة experimental-edge بالإضافة إلى edge. يشير كلا التكوينين إلى نفس الشيء، ولتبسيط الخيارات، سنقوم الآن بإرجاع خطأ إذا تم استخدام experimental-edge. لإصلاح هذا، قم بتحديث تكوين runtime إلى edge. تتوفر أداة تلقائية للقيام بذلك تلقائيًا.

طلبات fetch

لم تعد طلبات fetch مخزنة مؤقتًا افتراضيًا.

لاختيار تخزين طلبات fetch معينة مؤقتًا، يمكنك تمرير خيار cache: 'force-cache'.

app/layout.js
export default async function RootLayout() {
  const a = await fetch('https://...') // غير مخزن مؤقتًا
  const b = await fetch('https://...', { cache: 'force-cache' }) // مخزن مؤقتًا

  // ...
}

لاختيار تخزين جميع طلبات fetch في تخطيط أو صفحة مؤقتًا، يمكنك استخدام خيار تكوين القطاع export const fetchCache = 'default-cache' خيار تكوين القطاع. إذا حددت طلبات fetch الفردية خيار cache، فسيتم استخدام ذلك بدلاً من ذلك.

app/layout.js
// نظرًا لأن هذا هو التخطيط الجذري، سيتم تخزين جميع طلبات fetch في التطبيق
// التي لا تحدد خيار التخزين المؤقت الخاص بها.
export const fetchCache = 'default-cache'

export default async function RootLayout() {
  const a = await fetch('https://...') // مخزن مؤقتًا
  const b = await fetch('https://...', { cache: 'no-store' }) // غير مخزن مؤقتًا

  // ...
}

معالجات المسار

لم تعد دوال GET في معالجات المسار مخزنة مؤقتًا افتراضيًا. لاختيار تخزين طرق GET مؤقتًا، يمكنك استخدام خيار تكوين المسار مثل export const dynamic = 'force-static' في ملف معالج المسار.

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

export async function GET() {}

ذاكرة التوجيه من جانب العميل

عند التنقل بين الصفحات عبر <Link> أو useRouter، لم تعد أجزاء الصفحة معاد استخدامها من ذاكرة التوجيه من جانب العميل. ومع ذلك، لا تزال معاد استخدامها أثناء التنقل للخلف وللأمام في المتصفح وللتخطيطات المشتركة.

لاختيار تخزين أجزاء الصفحة مؤقتًا، يمكنك استخدام خيار التكوين staleTimes:

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

module.exports = nextConfig

لا تزال التخطيطات وحالات التحميل مخزنة مؤقتًا ومعاد استخدامها أثناء التنقل.

next/font

تمت إزالة الحزمة @next/font لصالح next/font المدمج. تتوفر أداة تلقائية لإعادة تسمية الواردات الخاصة بك بأمان وتلقائيًا.

app/layout.js
// قبل
import { Inter } from '@next/font/google'

// بعد
import { Inter } from 'next/font/google'

bundlePagesRouterDependencies

أصبح experimental.bundlePagesExternals مستقرًا وتمت إعادة تسميته إلى bundlePagesRouterDependencies.

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

  // بعد
  bundlePagesRouterDependencies: true,
}

module.exports = nextConfig

serverExternalPackages

أصبح experimental.serverComponentsExternalPackages مستقرًا وتمت إعادة تسميته إلى serverExternalPackages.

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

  // بعد
  serverExternalPackages: ['package-name'],
}

module.exports = nextConfig

Speed Insights

تمت إزالة الأدوات التلقائية لـ Speed Insights في Next.js 15.

لاستمرار استخدام Speed Insights، اتبع دليل البدء السريع لـ Vercel Speed Insights.

تحديد الموقع الجغرافي في NextRequest

تمت إزالة خصائص geo و ip من NextRequest حيث يتم توفير هذه القيم من قبل مزود الاستضافة الخاص بك. يتوفر أداة تحويل الشفرة (codemod) لأتمتة عملية الترحيل هذه.

إذا كنت تستخدم Vercel، يمكنك بدلاً من ذلك استخدام دوال geolocation و ipAddress من @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)

  // ...
}