Next.js من Pages Router إلى App Router: دليل الانتقال لعام
٦ مايو ٢٠٢٦
ملخص
يستعرض هذا البرنامج التعليمي كيفية نقل تطبيق حقيقي يعتمد على Pages Router في Next.js (المجلد pages/) إلى App Router (المجلد app/) على إصدار Next.js 16.2.41 في حوالي خمس وأربعين دقيقة. ستسمح للموجهين (routers) بالتعايش معاً، وتشغيل أداة @next/codemod الرسمية لتحويل params / searchParams / cookies() / headers() / draftMode() من التزامن (sync) إلى عدم التزامن (async)2، ونقل _app.tsx و _document.tsx إلى ملف app/layout.tsx واحد، ونقل معالج pages/API/* واحد إلى App Router Route Handler، واستبدال next/router بخطافات (hooks) next/navigation الثلاثة، وتبديل getStaticPaths بـ generateStaticParams. بنهاية هذا الدليل، سيكون لديك تطبيق هجين يعمل بالفعل، بالإضافة إلى مسار واضح لحذف مجلد pages/ عند الانتهاء من نقل آخر مسار.
ما ستتعلمه
المتطلبات الأساسية
ثبّت هذه الإصدارات قبل البدء. الإصدارات الأقدم ستظهر أخطاء في واجهة التطبيق الثنائية (ABI) والإعدادات تبدو وكأنها أخطاء في عملية النقل ولكنها في الحقيقة عدم توافق في أدوات التطوير.
- Node.js 20.9.0 أو أحدث (Node.js 18 لم يعد مدعوماً في Next.js 16)3. إصدارات Node.js 22 و 24 LTS كلاهما مناسبان.
- TypeScript 5.1.0 أو أحدث3؛ أحدث إصدار مستقر في سلسلة 5.x أو 6.x يعمل بشكل جيد أيضاً.
- npm 10+ (أو إصدار حديث من pnpm أو yarn) — يتم تشغيل وصفات codemod عبر
npx. - مشروع Pages Router موجود حالياً على Next.js 14.x أو 15.x. إذا كنت تستخدم 13.x، فقم بتنفيذ
npm i next@15أولاً وحل أخطاء الأنواع (type errors) قبل الانتقال إلى App Router؛ خلط ترقيتين رئيسيتين في طلب سحب (PR) واحد هو أسرع طريقة لإضاعة يوم كامل. - تأكد من نظافة شجرة عمل Git. كل عملية codemod أدناه تقوم بتعديل الملفات؛ ستحتاج إلى التزام (commit) واحد قابل للتراجع لكل خطوة.
الخطوة 1 — تأكد من أن Next.js 16 هو ما تتوقعه
قم بتحديث التبعيات (dependencies) قبل لمس أي كود مصدري. ستقوم أداة الترقية @next/codemod بتعديل package.json و tsconfig.json وحفنة من ملفات الإعداد نيابة عنك.
# داخل جذر المشروع
npx @next/codemod@latest upgrade latest
هذه العملية تفاعلية — ستسألك عن الإصدار المستهدف (اختر latest، والذي يشير إلى 16.2.4 اعتباراً من 15 أبريل 20261) وعن مدير الحزم الذي ستستخدمه. بعد الانتهاء، تحقق من الإصدارات المثبتة:
node -p "require('./package.json').dependencies.next"
# "16.2.4"
node -p "require('./package.json').dependencies.React"
# "19.2.5" — يستخدم App Router واجهة React 19 المستقرة داخلياً[^5]
npx tsc --version
# الإصدار 5.x أو 6.x — أي شيء من 5.1.0 فصاعداً يفي بالحد الأدنى
إذا تم تثبيت next بإصدار 15.x، فهذا يعني أن أداة الترقية اختارت بصمت تثبيت إصدار محافظ. قم بفرض التحديث يدوياً:
npm i next@16.2.4 React@19.2 React-dom@19.2
لا تقم بتشغيل next dev بعد — ستواجه التغييرات الجذرية المتعلقة بـ async-API قبل أن تنتهي من عملية النقل. الخطوة 2 ستعالج ذلك.
الخطوة 2 — تطبيق codemod لطلبات API غير المتزامنة
قدم إصدار Next.js 15 كلاً من cookies() و headers() و draftMode() و params و searchParams كدوال غير متزامنة (async) مع توفير طبقة توافق تزامنية. أما إصدار Next.js 16 فقد أزال هذه الطبقة تماماً، لذا فإن أي وصول تزامني سيؤدي إلى خطأ2. تتولى أداة codemod معالجة هذا الأمر آلياً:
npx @next/codemod@latest next-async-request-API .
تقوم الأداة بفحص كل ملف TS/JS في المجلد الحالي وتعديل أربعة أنماط:
- مكونات الخادم (Server Components) التي تقرأ
props.paramsمباشرة ستقوم الآن بعملawait props.params. - الصفحات ومعالجات المسارات (Route Handlers) التي تقرأ
searchParamsستضيف وبالمثلawait. - استدعاءات
cookies()/headers()/draftMode()منnext/headersستصبحawait cookies()وما شابهها. هذه الواجهات البرمجية الثلاث تعمل على الخادم فقط — استدعها من مكونات الخادم، أو معالجات المسارات، أو إجراءات الخادم (Server Actions). لا يمكن استدعاؤها من مكونات العميل (Client Components)، وداخل البرمجيات الوسيطة (middleware) تستخدمrequest.cookiesوrequest.headersعلى كائنNextRequestبدلاً من ذلك4. - مكونات العميل التي تستقبل
paramsأوsearchParamsكخصائص (والتي أصبحت الآن من نوعPromise<...>) سيتم استخراج قيمتها باستخدامuse()من React:const { slug } = use(params)5.
قم بتشغيل git diff بعد الـ codemod وألقِ نظرة سريعة. التحويل حتمي ولكنه قد يفشل في مكانين يستحقان الإصلاح اليدوي:
- الدوال التي تقوم بتفكيك (destructure)
paramsفي توقيعها، مثلfunction Page({ params }: { params: { slug: string } }). عادةً ما تعيد الأداة كتابة التفكيك ولكنها أحياناً تترك تعريف النوع بالشكل التزامني؛ قم بتحديث النوع إلىPromise<{ slug: string }>. - الأماكن التي تم فيها استخدام
params.slugداخل رد نداء (callback) غير متزامن (مثل سلسلة.then()أو تبعياتuseMemo). ستواجه خطأ TypeScript هناك حتى تقوم برفعawaitإلى أقرب حدودasync.
بعد التنظيف، قم بتشغيل npx tsc --noEmit وأصلح كل خطأ فات الأداة. هذه هي الخطوة الأكثر تأثيراً في عملية النقل بأكملها؛ قم بها قبل إضافة أي ملفات App Router جديدة حتى تتمكن من تحديد سبب أي مشكلة قد تطرأ.
الخطوة 3 — السماح بتعايش pages/ و app/
يدعم Next.js صراحةً تشغيل كلا الموجهين في نفس المشروع، وقد التزم الفريق بدعم Pages Router على المدى الطويل6. هذا التعايش هو ما يجعل النقل التدريجي آمناً.
أنشئ مجلد app/ في جذر المشروع (بجانب مجلد pages/) وأضف تخطيطاً جذرياً (root layout) مؤقتاً. يحل التخطيط الجذري محل كل من pages/_app.tsx و pages/_document.tsx بملف واحد7:
// app/layout.tsx
import './globals.css';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'My App',
description: 'Now serving from app/ and pages/',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
انسخ ملفات CSS العالمية من pages/_app.tsx إلى app/globals.css (أو أي مسار يشير إليه الاستيراد). انقل أي كتل <Script strategy="beforeInteractive" /> من _document.tsx إلى ملف التخطيط الجذري نفسه — يقوم Next.js تلقائياً بحقن نصوص beforeInteractive في وسم <head> الخاص بالمستند بغض النظر عن مكان وضع عنصر <Script> داخل التخطيط، ولكن ملف التخطيط هو المكان الوحيد المسموح فيه باستخدام هذه الاستراتيجية8. احتفظ بملفي _app.tsx و _document.tsx في مكانهما حالياً — فهما لا يزالان يخدمان المسارات التي لم يتم نقلها بعد.
تحقق من التعايش: قم بتشغيل npm run dev، ثم قم بزيارة أي رابط موجود في Pages Router. يجب أن يظهر تماماً كما كان من قبل. لا يتجاوز الموجهان أحدهما الآخر بصمت — إذا كان كل من app/foo/page.tsx و pages/foo.tsx يشيران إلى /foo، فسيفشل بناء Next.js مع ظهور خطأ "Conflicting routes" (مسارات متضاربة)9، لذا فإن نمط النقل الآمن هو حذف ملف pages/foo.tsx القديم في نفس الالتزام (commit) الذي يضيف app/foo/page.tsx.
الخطوة 4 — نقل أول مسار نهائي (Leaf Route)
اختر مسارًا منخفض المخاطر لترحيله أولاً — صفحة "من نحن" (About) ثابتة، أو عرض قائمة لا يستخدم getServerSideProps، أو صفحة تفاصيل يتم جلب بياناتها مرة واحدة. النمط دائمًا هو:
- إنشاء
app/<route>/page.tsx. - نقل الـ JSX إليه.
- استبدال دوال جلب البيانات المساعدة بما يعادلها في App Router.
- حذف ملف
pages/<route>.tsxالقديم في النهاية، بعد أن يعمل المسار الجديد بشكل صحيح.
إليك صفحة ثابتة بنظام Pages Router:
// pages/blog/[slug].tsx — قبل
import type { GetStaticPaths, GetStaticProps } from 'next';
type Props = { post: { title: string; body: string } };
export const getStaticPaths: GetStaticPaths = async () => {
const slugs = await fetch('https://api.example.com/posts').then(r => r.json());
return {
paths: slugs.map((s: string) => ({ params: { slug: s } })),
fallback: 'blocking',
};
};
export const getStaticProps: GetStaticProps<Props> = async ({ params }) => {
const post = await fetch(`https://api.example.com/posts/${params!.slug}`).then(r => r.json());
return { props: { post }, revalidate: 60 };
};
export default function PostPage({ post }: Props) {
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
);
}
ما يعادلها في App Router أقصر ويتم تصييره بالكامل على الخادم:
// app/blog/[slug]/page.tsx — بعد
export const revalidate = 60; // ISR على مستوى الجزء (segment)
export async function generateStaticParams() {
const slugs: string[] = await fetch('https://api.example.com/posts').then(r => r.json());
return slugs.map(slug => ({ slug }));
}
export default async function PostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await fetch(`https://api.example.com/posts/${slug}`).then(r => r.json());
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
);
}
ثلاث قواعد تغطي تقريبًا كل حالة. تصبح getStaticProps مجرد await fetch عادية داخل Server Component من نوع async، مع التعامل مع الصفحة كثابتة افتراضيًا. وتتحول getStaticPaths إلى generateStaticParams، والتي تعيد مصفوفة مسطحة من كائنات المعاملات بدلاً من غلاف { params: { ... } } القديم10. أما getServerSideProps فتصبح إما export const dynamic = 'force-dynamic' في أعلى الملف، أو خيار cache: 'no-store' لكل عملية جلب — وكلاهما يعطل التخزين المؤقت.
بعد أن يتم تصيير الملف الجديد بشكل صحيح على نفس الرابط، قم بتنفيذ git rm pages/blog/[slug].tsx. كرر ذلك مع المسار التالي.
الخطوة 5 — نقل مسار API واحد إلى Route Handler
تتبع مسارات API نفس قواعد التعايش. يستمر pages/api/users/[id].ts في العمل حتى تقوم بإنشاء app/api/users/[id]/route.ts. الشكل الجديد يستخدم تصديرات بأسماء طرق HTTP ودوال NextRequest / NextResponse المساعدة11:
// pages/api/users/[id].ts — قبل
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}
const { id = req.query;
const user = await fetch(`https://api.example.com/users/${id}`).then(r => r.json());
return res.status(200).json(user);
}
// app/api/users/[id]/route.ts — بعد
import { NextRequest, NextResponse } from 'next/server';
export async function GET(
_request: NextRequest,
{ params : { params: Promise<{ id: string }> ,
) {
const { id = await params;
const user = await fetch(`https://api.example.com/users/${id}`).then(r => r.json());
return NextResponse.json(user, { status: 200 );
}
لاحظ ثلاثة تغييرات تواجه معظم الفرق. لم يعد الـ handler تصديرًا افتراضيًا (default export) — فكل طريقة HTTP مدعومة تحصل على تصدير خاص بها باسمها async function GET / POST / PUT / PATCH / DELETE / HEAD / OPTIONS، والطرق غير المدعومة تعيد 405 تلقائيًا. الجزء الديناميكي أصبح الآن Promise، تمامًا كما في الصفحات، لأن نفس قاعدة طلبات API غير المتزامنة تنطبق هنا. وتم استبدال res.status().json() بـ return NextResponse.json(body, { status })، وهو ما يتوافق بشكل أفضل مع revalidate، والترويسات (headers)، واستجابات البث (streaming responses).
تحقق باستخدام curl من نافذة أوامر أخرى:
curl -i http://localhost:3000/api/users/42
# HTTP/1.1 200 OK
# content-type: application/json
# {"id":42,"name":"Ada Lovelace"}
الخطوة 6 — استبدال next/router بـ next/navigation
أي Client Component يستورد next/router سيستمر في العمل فقط طالما يتم تصيير هذا المكون من خلال مسار في pages/. بمجرد نقله تحت app/، يجب أن ينتقل إلى next/navigation ويتم تقسيم الاستيراد إلى ثلاثة خطافات (hooks)12:
// قبل — pages/<anywhere>.tsx
'use client';
import { useRouter } from 'next/router';
export function SortToggle() {
const router = useRouter();
const sort = (router.query.sort as string) ?? 'newest';
return (
<button onClick={() => router.push({ query: { sort: 'oldest' } })}>
Sort: {sort}
</button>
);
}
// بعد — تحت app/
'use client';
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
export function SortToggle() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const sort = searchParams.get('sort') ?? 'newest';
const flip = () => {
const next = new URLSearchParams(searchParams);
next.set('sort', 'oldest');
router.push(`${pathname?${next.toString()`);
};
return <button onClick={flip>Sort: {sort</button>;
}
جدول التحويل قصير ويستحق الحفظ: يصبح router.pathname هو usePathname()، ويصبح router.query لسلاسل الاستعلام (query strings) هو useSearchParams()، ويصبح router.query للأجزاء الديناميكية هو useParams(). جميع الخطافات الثلاثة مخصصة لـ Client Components فقط وتتطلب 'use client' في أعلى الملف. لا تزال router.push / router.replace موجودة في useRouter() من next/navigation ولكنها تقبل رابط URL نصي بدلاً من كائن استعلام — قم ببناء الرابط بنفسك باستخدام URLSearchParams.
الخطوة 7 — إصلاح فخ legacyBehavior في Link
أسقط React 16 دعم خاصية legacyBehavior في <Link> تمامًا13. أي كود لا يزال يغلف <a> داخل <Link legacyBehavior> سيتم بناؤه بنجاح في الإصدار 15.x ولكنه سيتعطل بمجرد الانتقال إلى 16.0. الإصلاح آلي:
// قبل
<Link href="/about" legacyBehavior>
<a className="nav-link">About</a>
</Link>
// بعد
<Link href="/about" className="nav-link">About</Link>
أصبحت أسماء الفئات (Class names)، وخصائص target، و rel توضع الآن على <Link> نفسه (حيث يقوم بتصيير <a> تلقائيًا). المكونات المخصصة التي كانت تغلف رابطًا وتستخدم passHref تحتاج إلى أكبر قدر من التفكير — فبدون legacyBehavior، غالبًا ما ستنتهي بتصيير زوج غير صالح من <a><a></a></a>. الإصلاح الأنظف هو وضع <Link> خارج الغلاف وتنسيق العنصر الداخلي بدلاً من ذلك.
يمكن لعملية بحث سريعة (grep) العثور على كل الحالات المخالفة:
grep -rn "legacyBehavior" app/ pages/ components/
يوجد أيضًا codemod مخصص يتعامل مع الحالات البسيطة:
npx @next/codemod@latest new-link .
يقوم الـ codemod بإعادة كتابة الأغلفة الواضحة تلقائيًا؛ أما أغلفة المكونات المخصصة فلا تزال بحاجة إلى مراجعة يدوية. أي شيء يتبقى بعد هاتين الخطوتين يجب إعادة كتابته قبل أن ينجح أمر next build.
التحقق
قم بتشغيل اختبارات التحقق الثلاثة للترحيل بالترتيب:
# 1. فحص الأنواع (Type check) يمر — يكتشف تحويلات API غير المتزامنة المفقودة
npx tsc --noEmit
# 2. فحص الكود (Lint) يمر — يكتشف استيرادات next/router الضالة تحت app/
npx next lint
# 3. بناء الإنتاج ينجح — يكتشف legacyBehavior والتغييرات الأخرى الخاصة بالإصدار 16
npm run build
# 4. تجربة مسار تم ترحيله بالكامل
npm start &
curl -s http://localhost:3000/blog/hello-world | grep -c "<article>"
# المتوقع: 1
إذا فشل next build مع شكوى من فقدان معامل slug لـ /blog/[slug]، فمن المرجح أن generateStaticParams قد أعادت شكل الغلاف القديم — قم بتبسيطه إلى [{ slug: 'foo' }]، وليس [{ params: { slug: 'foo' } }]10.
استكشاف الأخطاء وإصلاحها
خطأ "Dynamic APIs are Asynchronous" (صفحة التوثيق موجودة في nextjs.org/docs/messages/sync-dynamic-apis)14 — الـ codemod فاته أحد أماكن الاستدعاء. أضف await في Server Components، و Route Handlers، و Server Actions. بالنسبة لخصائص params / searchParams في Client Components، قم بفكها باستخدام use() من React. هذا هو الخطأ الأكثر شيوعًا بعد الخطوة 2.
خطأ من نوع useRouter is not a function داخل app/ — لقد قمت بالاستيراد من next/router. انتقل إلى import { useRouter } from 'next/navigation'. يتشارك خطافا useRouter في الاسم ولكن لهما أشكال إرجاع مختلفة؛ الخلط بينهما يؤدي إلى تعطل وقت التشغيل مع رسالة خطأ مضللة.
عدم تطابق الترطيب (Hydration mismatch) بعد نقل مزودي _app.tsx إلى app/layout.tsx — يجب تشغيل مزودي سياق (Context Providers) React على العميل، ولكن app/layout.tsx هو Server Component افتراضيًا. قم بتغليف المزودين في مكون منفصل من نوع 'use client' (مثلاً app/providers.tsx) واستورده في التخطيط الجذري (root layout).
فشل البناء مع رسالة حول إزالة/إهمال legacyBehavior — أكمل الخطوة 7 قبل إعادة المحاولة. يشير الخطأ إلى مسار ملف؛ أعد تشغيل grep من الخطوة 7 إذا كان هناك أي شيء لا يزال مفقودًا.
ISR لا يتحدث بعد تعديل revalidate — ذاكرة التخزين المؤقت للبناء تحت .next/cache تبقى عبر عمليات next build. احذف .next/cache وأعد البناء؛ نافذة إعادة التحقق (revalidate window) الجديدة تنطبق فقط على الصفحات التي تم بناؤها بعد التغيير.
الخطوات التالية
بمجرد أن تصبح كل المسارات موجودة تحت app/، قم بحذف pages/_app.tsx، و pages/_document.tsx، ومجلد pages/ الذي أصبح فارغاً الآن في "commit" نهائي للتنظيف. من هنا، يفتح App Router بقية مميزات Next.js 16: البث (streaming) باستخدام loading.tsx، والمسارات المتوازية (parallel routes)، والمسارات الاعتراضية (intercepting routes)، وبدائيات التخزين المؤقت (caching primitives) الجديدة. لمزيد من أنماط App Router المتقدمة على نطاق واسع، راجع إتقان أنماط Next.js App Router لتطبيقات الويب القابلة للتوسع؛ وللتعرف على نموذج الرندر الذي يجعل Server Components تتألق، راجع React Server Components: مستقبل الرندر السلس؛ وإذا كنت تضيف ميزات الذكاء الاصطناعي فوق التطبيق الذي تم ترحيله، فإن إتقان Vercel AI SDK v6 يربط بدائيات البث الجديدة مع App Router.
Footnotes
-
Next.js 16 release blog and 16.2 release notes — https://nextjs.org/blog/next-16 and https://nextjs.org/blog/next-16-2 ↩ ↩2
-
"Dynamic APIs are Asynchronous" + Codemods reference — https://nextjs.org/docs/messages/sync-dynamic-apis and https://nextjs.org/docs/app/guides/upgrading/codemods ↩ ↩2
-
Next.js 16 upgrade guide (Node.js 20.9 minimum, TypeScript 5.1 minimum) — https://nextjs.org/docs/app/guides/upgrading/version-16 ↩ ↩2
-
cookies()API reference (Server Components / Server Functions / Route Handlers / middleware) — https://nextjs.org/docs/app/API-reference/functions/cookies ↩ -
page.jsfile-system convention reference (Promise-typedparams/searchParamsanduse()unwrapping in Client Components) — https://nextjs.org/docs/app/API-reference/file-conventions/page ↩ -
Vercel team statement on long-term Pages Router support — https://GitHub.com/vercel/Next.js/discussions/56655 ↩
-
"Migrating: App Router" official guide (root layout replaces _app + _document) — https://nextjs.org/docs/app/guides/migrating/app-router-migration ↩
-
next/scriptComponent reference (beforeInteractiveplacement rules) — https://nextjs.org/docs/app/API-reference/components/script ↩ -
Next.js project-structure reference (conflicting routes between
app/andpages/cause a build-time error) — https://nextjs.org/docs/app/getting-started/project-structure ↩ -
generateStaticParamsAPI reference — https://nextjs.org/docs/app/API-reference/functions/generate-static-params ↩ ↩2 -
"Getting Started: Route Handlers" — https://nextjs.org/docs/app/getting-started/route-handlers ↩
-
useRouter(App Router) reference + GitHub Discussion #48426 on thenext/routervsnext/navigationsplit — https://nextjs.org/docs/app/API-reference/functions/use-router and https://GitHub.com/vercel/Next.js/discussions/48426 ↩ -
GitHub Discussion #80179, "NextLink legacyBehavior deprecated and removed in next 16" — https://GitHub.com/vercel/Next.js/discussions/80179 ↩
-
"Dynamic APIs are Asynchronous" error reference — https://nextjs.org/docs/messages/sync-dynamic-apis ↩