React: البدء في استخدام Next.js

آخر تحديث: 2026-08-26

أنشأ توم موقع مدونة باستخدام create-react-app. بعد إطلاقه، وجد أن تحميل الشاشة الأولى يستغرق 3 ثوانٍ، وأن محركات البحث لا تستطيع فهرسة المحتوى، وأنه كان عليه تحديث ذاكرة التخزين المؤقتة لشبكة توزيع المحتوى (CDN) يدويًّا في كل مرة يقوم فيها بتحديث منشور. أدرك أن العرض من جانب العميل وحده لم يكن كافيًّا — كان بحاجة إلى إطار عمل قادر على العرض من جانب الخادم، والتوليد الثابت، والتحسين التلقائي. عندها ظهر Next.js في الصورة.

يُعد Next.js، الذي طورته وتقوم بصيانته شركة Vercel، الإطار الأكثر شيوعًا حاليًا لتطوير تطبيقات React متكاملة (full-stack). فهو يعالج المشكلات الأساسية التي لا تستطيع React، بصفتها مكتبة واجهة مستخدم بحتة تعمل من جانب العميل، التعامل معها: أنظمة التوجيه، والعرض من جانب الخادم، وإنشاء المواقع الثابتة، وتوجيه واجهات برمجة التطبيقات (API)، وتحسين الصور، وتحسين الخطوط، والبرمجيات الوسيطة، وغير ذلك الكثير. سواء كنت تقوم بإنشاء مدونة، أو موقع للتجارة الإلكترونية، أو تطبيق SaaS، أو تطبيق على مستوى المؤسسات، فإن Next.js يوفر لك أفضل الممارسات الجاهزة للاستخدام فورًا.

يبدأ هذا الدرس بتوضيح المفاهيم الأساسية والتصميم المعماري لـ Next.js. ستتعرف على الفروق بين أوضاع العرض الأربعة، وكيفية عمل «موجه التطبيق» (App Router)، وتقسيم المهام بين المكونات من جانب الخادم ومن جانب العميل. وتشكل هذه المفاهيم الأساسية ركيزة لمزيد من الدراسة حول استرجاع البيانات، والنشر، والعمل العملي الشامل على المشاريع. ومن خلال دراسة الحالة العملية التي أجراها توم حول عملية الترحيل، ستكتسب فهمًا واضحًا للمسار التقني الكامل للترحيل من CRA إلى Next.js.


1. ما ستتعلمه



2. الرسوم التخطيطية المفاهيمية

أثناء تعلمه لـ Next.js، رسم توم مخططًا انسيابيًا لفهم مسار معالجة الطلبات. وعندما يصل طلب من المستخدم، تختار Next.js مسار عرض مختلفًا بناءً على تكوين الصفحة (ثابت، أو ديناميكي، أو تزايدي)، لتقوم في النهاية بإرجاع كود HTML وتمكين التفاعل من جانب المتصفح.

يوضح هذا الرسم البياني أربع نقاط قرار رئيسية: أولاً، تحديد نوع الصفحة (ثابتة/ديناميكية/تدريجية/من جانب العميل)؛ ثم اختيار محرك العرض المطابق؛ وإنشاء كود HTML وإرساله إلى المتصفح؛ وأخيرًا، استخدام عملية «الترطيب» لتمكين مكونات العميل الموجودة على الصفحة من أن تصبح تفاعلية.

100%
flowchart LR
    A[User Request] --> B{Next.js Route Matching}
    B -->|Static Page| C[SSG<br/>Generated during build]
    B -->|Dynamic Pages| D[SSR<br/>Render on Request]
    B -->|Incremental Update| E[ISR<br/>Re-verify on Demand]
    B -->|Client Interaction| F[CSR<br/>Browser Execution]
    C --> G[Back HTML]
    D --> G
    E --> G
    F --> G
    G --> H[Hydration<br/>Activate Interaction]
    H --> I[Client Component<br/>Run JS]


3. سيناريو واقعي

تم تطوير مدونة توم في الأصل باستخدام create-react-app، حيث كانت جميع الصفحات تُعرض على جانب العميل. وعندما يفتح المستخدمون مقالًا، كانوا يقومون أولاً بتنزيل قالب HTML فارغ، ثم ينتظرون حتى ينتهي تحميل جافا سكريبت قبل استرداد البيانات من واجهة برمجة التطبيقات (API) لعرض المحتوى. وكان هذا الأمر كارثةً لموقع ويب يعتمد على المحتوى — حيث لم ترَ برامج الزحف التابعة لمحركات البحث سوى صفحات فارغة، وكان وقت ظهور المحتوى الأول (TFC) يصل إلى 3 ثوانٍ.

وبعد إجراء بحثه، توصل إلى أن الأمر يتطلب اتباع نهج «العرض الهجين»: حيث يتم إنشاء منشورات المدونة بشكل ثابت (يتم إنشاء كود HTML أثناء عملية البناء وتوزيعه عبر شبكة توزيع المحتوى CDN)، بينما يتم عرض صفحات ملفات تعريف المستخدمين من جانب الخادم (مع إنشاء أحدث البيانات لكل طلب)، ويتم عرض قسم التعليقات من جانب العميل (مع معالجة التفاعلات داخل المتصفح). ويصادف أن Next.js يوفر كل ذلك.

أمضى أسبوعًا في ترحيل مدونته من CRA إلى Next.js، حيث قام بثلاثة أمور محددة: أولًا، حوّل صفحات المنشورات إلى SSG، مستخدمًا generateStaticParams لتوليد كود HTML لجميع صفحات المنشورات أثناء عملية البناء؛ وبعد النشر على Vercel، تقوم شبكة CDN بتقديم الصفحات المُعالجة مسبقًا مباشرةً، مما قلل وقت ظهور المحتوى الأول (TFC) إلى 0.3 ثانية. ثانيًا، قام بتحويل صفحات ملفات تعريف المستخدمين إلى SSR، حيث يتم جلب أحدث البيانات من قاعدة البيانات مع كل طلب؛ ثالثًا، تم تنفيذ قسم التعليقات باستخدام مكونات من جانب العميل، بحيث يتم تحميل جافا سكريبت التفاعلي فقط من جانب المتصفح. بعد عملية النقل، تحسنت درجة تحسين محركات البحث (SEO) للموقع على Google Lighthouse من 45 إلى 98، وانخفض وقت ظهور المحتوى الأول (TFC) بنسبة 90%.

كما واجه توم عددًا لا بأس به من العقبات أثناء عملية الترحيل. على سبيل المثال، استخدم في البداية «موجه الصفحات» (Pages Router) في الدليل pages/، لكنه اكتشف لاحقًا أن «موجه التطبيق» (App Router) يعمل بشكل أفضل، لذا أمضى وقتًا إضافيًا في الترحيل إليه. ويوصي باستخدام «App Router» مباشرةً في المشاريع الجديدة لتجنب الحاجة إلى الترحيل مرة ثانية. كما اكتشف أن useRouter وusePathname لا يمكن استخدامهما مباشرةً في مكونات الخادم (Server Components)؛ بل يجب استخراج منطق التوجيه هذا إلى مكونات العميل (Client Components). وقد منحته هذه الدروس فهمًا عميقًا لفلسفة تصميم Next.js، وهي «الخادم أولاً، والعميل حسب الطلب».

(1) مقارنة بين أوضاع العرض

يوفر Next.js أربعة أوضاع للعرض، كل منها مصمم لحالات استخدام مختلفة. ويعد فهم الاختلافات بينها عاملاً أساسياً لاختيار البنية المناسبة.

CSR (العرض من جانب العميل): يقوم المتصفح بتنزيل هيكل HTML فارغ ثم ينفذ جافا سكريبت لعرض المحتوى. كانت مدونة توم تستخدم هذا النموذج في الأصل. تتمثل مزايا هذا النموذج في التفاعل السلس وتقليل الحمل على الخادم؛ أما عيوبه فهي بطء تحميل الشاشة الأولى وضعف تحسين محركات البحث (SEO). وهو مناسب للتطبيقات التفاعلية التي تتطلب تسجيل دخول المستخدم، مثل لوحات التحكم والواجهات الإدارية الخلفية.

SSR (العرض من جانب الخادم): في كل مرة يرسل فيها المستخدم طلبًا، يقوم الخادم ديناميكيًّا بإنشاء كود HTML الكامل وإرجاعه. استخدم توم هذه الطريقة لحل مشكلات تحسين محركات البحث (SEO) في مدونته، ولكن نظرًا لأن كل طلب يجب أن ينتظر حتى ينتهي الخادم من العرض قبل إرجاع الرد، فإن ذلك يضع عبئًا ثقيلًا على الخادم. وهي مناسبة للصفحات التي تتطلب بيانات في الوقت الفعلي، مثل مواقع الأخبار وصفحات المنتجات في مواقع التجارة الإلكترونية.

SSG (إنشاء المواقع الثابتة): يقوم بإنشاء ملفات HTML لجميع الصفحات خلال مرحلة البناء؛ وبعد النشر على شبكة توزيع المحتوى (CDN)، يصل المستخدمون إلى الملفات الثابتة مباشرةً. ويوفر هذا أسرع سرعة تحميل وأفضل تحسين لمحركات البحث (SEO)، لكن تحديثات المحتوى تتطلب إعادة بناء كاملة. وتعد صفحات مدونة «توم» مثالاً جيدًا على هذا النموذج — فبمجرد كتابة المقالة، يظل محتواها ثابتًا. وهو مناسب للمدونات ومواقع التوثيق وصفحات التسويق.

ISR (التوليد الثابت التزايدي): نسخة محسّنة من SSG تتيح إعادة التحقق من صحة صفحات محددة وتحديثها عند الطلب بعد عملية البناء، دون الحاجة إلى إعادة بناء الموقع بالكامل. على سبيل المثال، إذا تم ضبط صفحة كتالوج منتجات «توم» على إعادة التحقق من صحتها كل 60 ثانية، فستظهر المنتجات الجديدة في غضون 60 ثانية من إضافتها. ويُعد هذا الخيار مثاليًّا لصفحات منتجات التجارة الإلكترونية والمواقع التي يتم تحديث محتواها بشكل متكرر.

يمكن تلخيص استراتيجية الاختيار على النحو التالي: استخدم SSG في السيناريوهات التي تعطي الأولوية للمحتوى، وSSR للبيانات في الوقت الفعلي، وCSR للسيناريوهات التي تتطلب تفاعلات مكثفة، وISR للسيناريوهات التي تتطلب تحديثات متكررة ولا ترغب فيها في إعادة بناء الموقع بالكامل.

جدول مقارنة أداء أوضاع العرض

المؤشر SSG ISR SSR CSR
سرعة تحميل الشاشة الأولى الأسرع سريع متوسط بطيء
ملاءمة محركات البحث عالية عالية عالية منخفضة
توقيت البيانات يتم تحديدها عند الإنشاء يتم تحديثها عند الطلب أحدث البيانات مع كل طلب بعد تحميل المتصفح
حمل الخادم لا شيء منخفض مرتفع منخفض
حالات الاستخدام المدونات/الوثائق التجارة الإلكترونية/الأخبار الصفحات المخصصة إدارة النظام الخلفي
متوسط زمن الاستجابة الأولي (TTFB) <50 مللي ثانية <100 مللي ثانية 200-500 مللي ثانية 200-500 مللي ثانية

▶ المثال 1: تطبيق أوضاع العرض الأربعة في Next.js

يوضح الكود أدناه كيفية تنفيذ أوضاع العرض الثلاثة في مدونة توم. لاحظ أن الدالة generateStaticParams يتم استدعاؤها أثناء عملية البناء وتُرجع جميع تركيبات المعلمات الممكنة؛ ويستخدم Next.js ذلك لإنشاء كود HTML ثابت لجميع الصفحات. أما الدالة dynamic = 'force-dynamic'، فهي تُجبر الصفحة على إعادة العرض مع كل طلب.

TSX
// === Static Generation SSG(Default behavior) ===
// app/blog/[slug]/page.tsx
// Automatically scan all articles during the build process slug,Generate the corresponding HTML Page
// After generation CDN Return static files directly,No server-side rendering required

export async function generateStaticParams() {
  // Called during build:Get a list of all articles
  const posts = await fetch('https://api.example.com/posts').then(r => r.json())
  // Back slug Array,Next.js It will do this for each slug Generate a page
  return posts.map((post: any) => ({ slug: post.slug }))
}

async function BlogPost({ params }: { params: { slug: string } }) {
  // Each page retrieves data independently when it is rendered.
  const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(r => r.json())
  return (
    <article>
      <h1>{post.title}</h1>
      <time>{post.publishedAt}</time>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}
export default BlogPost

// === Server-Side Rendering SSR(Dynamic Rendering) ===
// app/dashboard/page.tsx
// Every user request starts from API Get the latest data,Ensure the data is up to date

export const dynamic = 'force-dynamic' // Force the static cache to close,Leaving every time SSR

async function DashboardPage() {
  // This will be executed with every request fetch
  const stats = await fetch('https://api.example.com/dashboard/stats').then(r => r.json())
  return (
    <div>
      <h1>Real-Time Dashboard</h1>
      <p>Current Online Users:{stats.onlineUsers}</p>
      <p>Today's Orders:{stats.todayOrders}</p>
      <p>Income for This Month:${stats.monthlyRevenue}</p>
    </div>
  )
}
export default DashboardPage

// === Incremental Static Generation ISR ===
// app/products/[id]/page.tsx
// Generate static pages during the build process,But every 60 The first visit after X seconds will trigger a regenerate.

async function ProductPage({ params }: { params: { id: string } }) {
  const product = await fetch(`https://api.example.com/products/${params.id}`, {
    next: { revalidate: 60 } // ISR:60 Please try again in a few seconds.
  }).then(r => r.json())
  return (
    <div>
      <h1>{product.name}</h1>
      <p>Price:${product.price}</p>
      <p>Inventory:{product.stock}</p>
      <p>Description:{product.description}</p>
    </div>
  )
}
export default ProductPage

في مدونته، اختار توم اتباع نهج هجين يجمع بين SSG و ISR: فهو يستخدم SSG لصفحات المقالات لضمان التحميل السريع للصفحة الرئيسية، ويستخدم ISR لصفحات قوائم المنتجات، التي تُحدَّث الأسعار والمخزون فيها كل 60 ثانية. ويضمن هذا النهج الحصول على نتائج جيدة في تحسين محركات البحث (SEO) مع الحفاظ في الوقت نفسه على حداثة البيانات.

(2) توجيه ملفات «App Router»

يقدم Next.js 13+ ميزة «App Router»، التي تقوم تلقائيًا بإنشاء مسارات بناءً على نظام الملفات. كل ما عليك فعله هو إنشاء مجلد باسم app/ وملف باسم page.tsx داخل هذا المجلد، وسيقوم Next.js تلقائيًا بتعيين مسارات عناوين URL المقابلة. لم تعد بحاجة إلى صيانة ملفات تكوين المسارات يدويًّا — فهياكل الدلائل هي نفسها هياكل المسارات.

أنواع الملفات الأساسية

التوجيه الديناميكي والأوضاع المتقدمة

استخدم صيغة [param] لإنشاء مسارات ديناميكية؛ على سبيل المثال، تتطابق app/blog/[slug]/page.tsx مع /blog/hello-world. استخدم صيغة التعميم [...param] لمطابقة المسارات متعددة المستويات؛ على سبيل المثال، تتطابق app/docs/[...slug]/page.tsx مع /docs/guide/getting-started/installation.

تدعم ملفات التخطيط التخطيطات المتداخلة بعمق — حيث يحيط تخطيط «أب» بمحتوى الصفحات «الفرعية». ولا يلزم كتابة الأقسام الشائعة، مثل شريط التنقل والشريط الجانبي والتذييل، سوى مرة واحدة؛ حيث تتولى Next.js إدارة الحالة الدائمة للتخطيط تلقائيًا. وعند التبديل بين الصفحات، لا يتم إعادة عرض التخطيط؛ بل يتم تحديث الصفحات الفرعية فقط.

▶ المثال 2: هيكل توجيه المدونة الكامل

يُظهر هذا هيكل الدلائل الكامل لـ «App Router» الخاص بمدونة توم. لاحظ أن أسماء الملفات الخاصة في كل مجلد (page، layout، loading، error، not-found) تتبع قواعد Next.js — حيث يتعرف الإطار تلقائيًا على هذه الملفات ويخصص لها سلوكيات توجيه محددة. يقع dashboard/layout.tsx داخل المجلد الجذر layout.tsx، مما يشكل تخطيطًا من مستويين: تتكون الطبقة الخارجية من شريط التنقل العام وتذييل الصفحة، بينما تحتوي الطبقة الداخلية على الشريط الجانبي الخاص بلوحة التحكم.

BASH
app/
├── page.tsx                # → / Home
├── layout.tsx              # → Global Layout(Navigation + Footer)
├── loading.tsx             # → Global Loading State(Skeleton screen during page transitions)
├── error.tsx               # → Global Error Page(500 A Flawed Fallback UI)
├── not-found.tsx           # → 404 Page
├── about/
│   └── page.tsx            # → /about About This Page
├── blog/
│   ├── page.tsx            # → /blog Article List Page
│   └── [slug]/             # → Dynamic routing: matches /blog/any-value
│       ├── page.tsx        # → /blog/hello-world Article Details
│       └── loading.tsx     # → Custom Loading State for Article Detail Pages
├── dashboard/
│   ├── page.tsx            # → /dashboard Dashboard Overview
│   ├── layout.tsx          # → Dashboard-Exclusive Layout(With sidebar navigation)
│   └── settings/
│       └── page.tsx        # → /dashboard/settings Settings Page
└── api/
    └── hello/
        └── route.ts        # → /api/hello API Endpoint

جدول مرجعي لرسم خرائط المسارات

مسار الملف عنوان URL المقابل الوصف
app/page.tsx / الصفحة الرئيسية
app/about/page.tsx /about المسارات الثابتة
app/blog/[slug]/page.tsx /blog/:slug التوجيه الديناميكي
app/blog/[slug]/loading.tsx /blog/:slug حالات التحميل على نفس المسار
app/dashboard/layout.tsx /dashboard/* تخطيط متداخل
app/api/hello/route.ts /api/hello نقطة نهاية واجهة برمجة التطبيقات
TSX
// app/layout.tsx - Global Layout
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh">
      <body>
        <header>
          <nav>
            <a href="/">Home</a>
            <a href="/blog">Blog</a>
            <a href="/about">About</a>
          </nav>
        </header>
        <main>{children}</main>
        <footer>&copy; 2026 Tom 's blog. All rights reserved.</footer>
      </body>
    </html>
  )
}

// app/dashboard/layout.tsx - Dashboard-Exclusive Layout(Nested within the global layout)
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <div style={{ display: 'flex' }}>
      <aside style={{ width: 240, background: '#f5f5f5', padding: 16 }}>
        <nav>
          <a href="/dashboard">Overview</a>
          <a href="/dashboard/settings">Settings</a>
        </nav>
      </aside>
      <section style={{ flex: 1, padding: 16 }}>{children}</section>
    </div>
  )
}

// app/blog/[slug]/loading.tsx - Article Details Loading...
export default function Loading() {
  return (
    <div style={{ padding: 24 }}>
      <div style={{ height: 32, width: '60%', background: '#eee', borderRadius: 4, marginBottom: 16 }} />
      <div style={{ height: 16, width: '30%', background: '#eee', borderRadius: 4, marginBottom: 24 }} />
      <div style={{ height: 200, background: '#eee', borderRadius: 4 }} />
    </div>
  )
}

(3) مكون الخادم ومكون العميل

يُعد «مكون الخادم» (Server Component) أحد التصاميم الثورية في Next.js. وهذه هي المرة الأولى في منظومة React التي يتم فيها تقسيم المكونات إلى بيئتي تنفيذ: بيئة جانب الخادم وبيئة جانب العميل. وبشكل افتراضي، تكون جميع المكونات الموجودة في «موجه التطبيق» (App Router) مكونات خادم — فهي تعمل على الخادم، ويمكنها الوصول مباشرةً إلى قاعدة البيانات ونظام الملفات ومتغيرات البيئة الحساسة، والأهم من ذلك أنها لا ترسل أي كود JavaScript إلى المتصفح.

عندما تحتاج إلى وظائف تفاعلية (مثل النقر على زر، أو إدخال نص، أو استخدام useState أو useEffect، أو الاستجابة لأحداث النافذة)، فإن المكون من جانب الخادم وحده لا يكفي. في هذه الحالة، تحتاج إلى إضافة إعلان 'use client' في أعلى الملف. سيقوم Next.js بعد ذلك بتمييز هذا المكون وجميع مكوناته الفرعية كمكونات عميل (Client Components) وتجميعها ليتم تنفيذها على جانب المتصفح.

ميزات مكون الخادم

ميزات مكون العميل

نماذج أفضل الممارسات

وهي تعتمد بنية متعددة الطبقات حيث يعمل «مكون الخادم» كحاوية، بينما يعمل «مكون العميل» كعنصر نهائي. ويتولى «مكون الخادم» في الطبقة الخارجية مسؤولية جلب البيانات والتحكم في التخطيط، في حين يتولى «مكون العميل» في الطبقة الداخلية مسؤولية العناصر التي تتطلب تفاعلًا فقط. وبهذه الطريقة، تتم معالجة معظم العمليات المنطقية وعمليات استرجاع البيانات على جانب الخادم، ولا يقوم المتصفح بتشغيل سوى الحد الأدنى الضروري من جافا سكريبت.

▶ المثال 3: التقسيم الأمثل للمسؤوليات بين مكونات الخادم والعميل

يوضح الكود أدناه النموذج الطبقي الموصوف في مدونة توم: «استرجاع البيانات على الخادم، والتفاعلات في المتصفح». PostsPage هو مكون خادم — فهو يسترد البيانات من الخادم، ويعرضها بتنسيق HTML، ويرسلها مباشرةً إلى المتصفح. يرى المستخدمون الصفحة كاملةً دون الحاجة إلى انتظار تحميل JavaScript. PostList هو مكون عميل — يستقبل البيانات التي أعدها الخادم، ويكون مسؤولاً فقط عن تفاعلات البحث والتصفية والحذف من جانب المتصفح. لاحظ أن 'use client' يُطبق فقط على «المكونات الطرفية» التي تتطلب تفاعلًا، وليس على الصفحة بأكملها.

TSX
// app/posts/page.tsx - Server Component(Default)
// This component runs on the server.,Do not send any JS Go to the browser
import PostList from './PostList'

async function PostsPage() {
  // Directly on the server fetch,Data is being compiled HTML It was already ready at that time
  // Users see the full page,None loading Status
  const posts = await fetch('https://api.example.com/posts').then(r => r.json())

  return (
    <div>
      <h1>All Articles</h1>
      <PostList posts={posts} />
    </div>
  )
}
export default PostsPage

// app/posts/PostList.tsx - Annotation 'use client' Become Client Component
// Only this file will be sent JS Go to the browser,The parent component that contains it(Server Component)No
'use client'
import { useState } from 'react'

interface Post {
  id: number
  title: string
  body: string
}

function PostList({ posts: initialPosts }: { posts: Post[] }) {
  // useState Only at 'use client' Available in the component
  const [posts, setPosts] = useState(initialPosts)
  const [search, setSearch] = useState('')

  const filtered = posts.filter(p =>
    p.title.toLowerCase().includes(search.toLowerCase())
  )

  function handleDelete(id: number) {
    setPosts(prev => prev.filter(p => p.id !== id))
  }

  return (
    <div>
      <input
        type="text"
        placeholder="Search Articles..."
        value={search}
        onChange={e => setSearch(e.target.value)}
        style={{ width: '100%', padding: 8, marginBottom: 16, border: '1px solid #ddd', borderRadius: 4 }}
      />
      {filtered.map(post => (
        <div key={post.id} style={{
          border: '1px solid #ddd', padding: 16, marginBottom: 8, borderRadius: 8,
          display: 'flex', justifyContent: 'space-between', alignItems: 'center'
        }}>
          <div>
            <h2 style={{ margin: 0 }}>{post.title}</h2>
            <p style={{ margin: '8px 0', color: '#666' }}>{post.body}</p>
          </div>
          <button
            onClick={() => handleDelete(post.id)}
            style={{ padding: '4px 12px', background: '#ff4444', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}
          >
            Delete
          </button>
        </div>
      ))}
      {filtered.length === 0 && <p>No matching articles were found.</p>}
    </div>
  )
}
export default PostList

// pages/old-posts.js - Pages Router Pattern(Compatibility with Legacy Projects)
// If you are maintaining Next.js 12 or earlier projects,Pages Router Usage getStaticProps/getServerSideProps
export async function getStaticProps() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json())
  return { props: { posts }, revalidate: 60 }
}

export default function OldPostsPage({ posts }: { posts: Post[] }) {
  return (
    <div>
      <h1>Pages Router Pattern</h1>
      {posts.map(p => <p key={p.id}>{p.title}</p>)}
    </div>
  )
}

القيود المفروضة على مكونات الخادم مقابل مكونات العميل

الإمكانية مكون الخادم مكون العميل
استخدام useState/useEffect
استخدام onClick/onChange
الوصول المباشر إلى قاعدة البيانات
الوصول إلى متغيرات البيئة (بدون بادئة)
إرسال جافا سكريبت إلى المتصفح
استخدام async/await
استخدام useRouter/usePathname
استخدم requestAnimationFrame

مخطط انسيابي: مكون الخادم أم مكون العميل؟

TEXT 📖 للعرض فقط
Components require interaction(Click、Input、Scrolling, etc.)?
├── is  → Required useState/useEffect?
│   ├── Yes → add 'use client' → Client Component
│   └── No → only accept props for rendering?
│       ├── is  → Retain Server Component
│       └── No → add 'use client'
└── No → keep Server Component


4. خارطة طريق توم للتحول

لقد استعرضنا الآن جميع المفاهيم الأساسية لـ Next.js. والآن، دعونا نجمع هذه المعرفة معًا ونرى كيف قام توم بترحيل مدونته التي كانت تعمل بنظام CRA إلى Next.js خطوة بخطوة. وتُعد هذه العملية أيضًا النهج القياسي لتطبيق Next.js في مشاريعكم الخاصة.

يشرح توم العملية الكاملة لنقل مدونة من CRA إلى Next.js في خمس خطوات، تتوافق كل منها مع مفهوم رئيسي تمت تغطيته في هذا الدرس:


▶ المثال 4: ملفات «loading.tsx» و«error.tsx» في «App Router»

TSX
// app/dashboard/loading.tsx - Automatically Displayed Loading Status
export default function DashboardLoading() {
  return (
    <div style={{ padding: 24 }}>
      <div style={{ height: 32, width: 200, background: '#f0f0f0', borderRadius: 4, marginBottom: 16 }} />
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}>
        {[1, 2, 3].map(i => (
          <div key={i} style={{
            height: 120, background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
            backgroundSize: '200% 100%', borderRadius: 8, animation: 'shimmer 1.5s infinite',
          }} />
        ))}
      </div>
    </div>
  )
}

// app/dashboard/error.tsx - Automatically Displayed Error Status
'use client'
export default function DashboardError({ error, reset }) {
  return (
    <div style={{ padding: 40, textAlign: 'center' }}>
      <h2>Something went wrong!</h2>
      <p style={{ color: '#999' }}>{error.message}</p>
      <button onClick={reset} style={{ padding: '8px 24px', cursor: 'pointer' }}>Try again</button>
    </div>
  )
}

// app/dashboard/page.tsx - It will automatically use the one above loading and  error
async function DashboardPage() {
  const data = await fetch('https://api.example.com/dashboard').then(r => r.json())
  return (
    <div>
      <h1>Dashboard</h1>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}>
        {data.stats.map(stat => (
          <div key={stat.label} style={{ padding: 16, border: '1px solid #eee', borderRadius: 8 }}>
            <p style={{ color: '#999', margin: 0 }}>{stat.label}</p>
            <p style={{ fontSize: 24, fontWeight: 'bold', margin: '4px 0 0' }}>{stat.value}</p>
          </div>
        ))}
      </div>
    </div>
  )
}
export default DashboardPage

▶ المثال 5: التخطيطات والقوالب المتداخلة

TSX
// app/layout.tsx - Root Layout(Must have html and  body)
export default function RootLayout({ children }) {
  return (
    <html lang="zh">
      <body style={{ margin: 0, fontFamily: 'sans-serif' }}>
        {children}
      </body>
    </html>
  )
}

// app/(dashboard)/layout.tsx - Dashboard Layout(Route groups have no effect URL)
import Sidebar from '@/components/Sidebar'

export default function DashboardLayout({ children }) {
  return (
    <div style={{ display: 'flex', minHeight: '100vh' }}>
      <Sidebar />
      <main style={{ flex: 1, padding: 24 }}>
        {children}
      </main>
    </div>
  )
}

// app/(auth)/layout.tsx - Authentication Page Layout(Centered Card)
export default function AuthLayout({ children }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#f5f5f5' }}>
      <div style={{ background: 'white', padding: 32, borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.1)', width: 400 }}>
        {children}
      </div>
    </div>
  )
}

// app/(auth)/login/page.tsx - Login Page
function LoginPage() {
  return (
    <>
      <h2 style={{ textAlign: 'center' }}>Sign In</h2>
      <form style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
        <input type="email" placeholder="Email" style={{ padding: 8, borderRadius: 4 }} />
        <input type="password" placeholder="Password" style={{ padding: 8, borderRadius: 4 }} />
        <button type="submit" style={{ padding: 10, background: '#1890ff', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>
          Sign In
        </button>
      </form>
    </>
  )
}
export default LoginPage


❓ أسئلة شائعة

س ما هي العلاقة بين Next.js وReact؟
ج React هي مكتبة واجهة مستخدم مسؤولة عن عرض المكونات وإدارة الحالة؛ أما Next.js فهو إطار عمل متكامل (full-stack) مبني على React، ويحتوي على ميزات مدمجة مثل التوجيه (routing)، وSSR/SSG/ISR، وتوجيه واجهة برمجة التطبيقات (API)، وتحسين الصور، وتحسين الخطوط، والبرمجيات الوسيطة (middleware). يمكنك التفكير في الأمر على النحو التالي: يوفر React اللبنات الأساسية، بينما يوفر Next.js سلسلة الأدوات الكاملة لتجميع تلك اللبنات في تطبيق جاهز للإنتاج. يركز React على «كيفية عرض واجهة المستخدم»، بينما يتناول Next.js «كيفية بناء تطبيق ويب كامل».
س كيف أختار بين مكونات الخادم ومكونات العميل؟
ج أعطِ الأولوية لاستخدام مكونات الخادم (الخيار الافتراضي). لا تضف 'use client' إلا عندما يحتاج أحد المكونات إلى استخدام useState أو useEffect أو onClick أو واجهات برمجة تطبيقات المتصفح. من الأنماط الشائعة أن يقوم مكون الخادم بجلب البيانات وتمريرها إلى مكون عميل داخلي للتعامل مع التفاعلات. بهذه الطريقة، تتم معالجة استرجاع البيانات على الخادم، بينما يتم تشغيل منطق التفاعل في المتصفح — وهو ما يجمع بين مزايا كلا العالمين. من الأخطاء الشائعة بين المبتدئين إضافة 'use client' إلى الصفحة بأكملها — حيث يجب تطبيقه فقط على أصغر العقد الطرفية التفاعلية. إذا وجدت مكونًا لا يتطلب تفاعلًا ولا حالة، فلا تضف 'use client'.
س أيهما يقدم أداءً أفضل، SSR أم SSG؟
ج تقدم تقنية SSG أداءً أفضل لأن كود HTML يتم إنشاؤه أثناء عملية البناء، كما أن شبكة توزيع المحتوى (CDN) تقدم الملفات الثابتة مباشرةً دون أي عبء حسابي من جانب الخادم. ومع ذلك، فإن تقنية SSG غير مناسبة للصفحات التي يتغير محتواها بشكل متكرر. أما تقنية SSR فتقوم بإعادة العرض مع كل طلب، مما يؤدي إلى زمن انتقال أعلى، لكن البيانات تكون محدثة دائمًا. ويُعد ISR حلاً وسطًا — فهو سريع مثل SSG في معظم الأوقات، ويعيد إنشاء المحتوى عند الطلب عندما تكون هناك حاجة إلى تحديثات. وفي المشاريع الواقعية، عادةً ما يتم استخدام نهج هجين: SSG لصفحات التسويق، وISR لصفحات المنتجات، وSSR للوحة تحكم المستخدم.
س ما الفرق بين «App Router» و«Pages Router»؟
ج «Pages Router» هي طريقة التوجيه المستخدمة في Next.js 12 والإصدارات الأقدم. وهي تستخدم الدليل pages/ وتقوم بتعيين المسارات بناءً على أسماء الملفات. أما «موجه التطبيقات» (App Router) فهو نظام التوجيه الجديد الذي تم تقديمه في Next.js 13. ويستخدم الدليل app/ ويدعم ميزات جديدة مثل التخطيطات المتداخلة ومكونات الخادم وعرض البث المباشر. بالنسبة للمشاريع الجديدة، يُنصح باستخدام «موجه التطبيق» مباشرةً؛ أما المشاريع الحالية فيمكن ترحيلها تدريجيًا. وقد تم استبدال بنية getStaticProps/getServerSideProps في «موجه الصفحات» بـ async component + fetch في «موجه التطبيق». يمكن أن يتعايش نظاما التوجيه معًا، وأثناء عملية الترحيل، يمكنك نقل الصفحات تدريجيًا من «موجه الصفحات» إلى «موجه التطبيق».
س هل يجب نشر مشاريع Next.js على Vercel؟
ج ليس بالضرورة. على الرغم من أن Vercel هي الشركة المطورة لـ Next.js وتقدم تجربة نشر أكثر سلاسة (النشر بنقرة واحدة، والتوسع التلقائي، وشبكة الحافة)، إلا أنه يمكن أيضًا نشر Next.js على منصات أخرى. يمكنك استخدام next build && next start للنشر على أي خادم Node.js، أو استخدام Docker للنشر في حاويات. كما تدعم Netlify وAWS Amplify وCloudflare Pages نشر Next.js. ومع ذلك، إذا كنت تستخدم ميزات متقدمة مثل ISR والبرمجيات الوسيطة، فإن Vercel توفر أفضل توافق.
س ماذا لو بدأت باستخدام «موجه الصفحات» (Pages Router) ثم أردت لاحقًا الترحيل إلى «موجه التطبيق» (App Router)؟
ج يمكن أن يتعايش نظاما التوجيه هذان في نفس المشروع. يمكنك نقل الصفحات تدريجيًّا من pages/ إلى app/، مع التحقق من كل صفحة أثناء نقلها — فلا داعي لترحيل كل شيء دفعة واحدة. أثناء الترحيل، يرجى ملاحظة ما يلي: قم بتغيير getStaticProps إلى fetch مباشرةً داخل مكون الخادم؛ وانقل التخطيط إلى layout.tsx، الذي تم إنشاؤه ضمن دليل app/؛ وقم بترحيل المنطق من pages/_app.tsx إلى الجذر layout.tsx. نوصي بالبدء بالصفحات الأبسط والتعامل مع الصفحات الأكثر تعقيدًا بمجرد اكتساب بعض الخبرة.
س كيف يمكنني استرداد معلمات URL في مكون الخادم؟
ج تسترد مكونات الخادم معلمات المسار الديناميكية عبر الخاصية params، ومعلمات سلسلة الاستعلام عبر الخاصية searchParams. على سبيل المثال، function Page({ params, searchParams }: { params: { slug: string }, searchParams: { q: string } }). ومع ذلك، لاحظ أنه لا يمكنك استخدام useRouter أو usePathname في مكون الخادم — فهذه الخطافات متاحة فقط في مكونات العميل.

📖 ملخص


📝 تمارين

  1. استخدم npx create-next-app@latest my-blog لإنشاء مشروع جديد، مع التأكد من استخدام بنية مجلدات «App Router». ضمن app/، أنشئ ثلاثة مسارات للصفحات: /about و/blog و/blog/[slug]. يجب أن تحتوي كل صفحة على عنوان واحد على الأقل ووصف. قم بتشغيل npm run dev وقم بزيارة كل مسار (/about و/blog و/blog/test-article) للتحقق من صحة التعيينات.

  2. قم بتنفيذ تخطيط عام (app/layout.tsx) يتضمن شريط تنقل علوي (يحتوي على ثلاثة روابط: الصفحة الرئيسية، والمدونة، و«نبذة عنا») وتذييل الصفحة. بعد ذلك، قم بإنشاء تخطيط متداخل للمسار /blog/* لعرض جدول محتويات المقالة على الجانب الأيسر من صفحة تفاصيل المقالة. افتح React DevTools في متصفحك للتحقق من صحة التسلسل الهرمي للتداخل في التخطيط، ومن أن شريط التنقل والشريط الجانبي يحتفظان بحالتهما دون إعادة عرض عند التبديل بين الصفحات.

  3. قم بتغيير الصفحة الرئيسية للمدونة إلى مكون خادم يسترد قائمة المقالات مباشرةً من واجهة برمجة تطبيقات JSONPlaceholder (https://jsonplaceholder.typicode.com/posts) ويعرضها. بعد ذلك، أنشئ مكون عميل لتنفيذ وظائف البحث عن الكلمات المفتاحية والتصفية، مما يتيح للمستخدمين البحث عن المقالات وتصفيتها في الوقت الفعلي من جانب المتصفح. تأكد من أن مكون الخادم يتولى استرداد البيانات، بينما يتولى مكون العميل منطق التصفية التفاعلي فقط. بمجرد الانتهاء من ذلك، أضف loading.tsx (لعرض شاشة مؤقتة) وerror.tsx (لعرض رسائل الخطأ وزر إعادة المحاولة) لتجربة التكوين الكامل لمقطع التوجيه.

  4. اختياري: انشر مدونتك على Vercel (مجانًا)، وراقب عملية إعادة التحقق من صلاحية ISR في وحدة التحكم الخاصة بـ Vercel — بعد تعديل البيانات التي تعيدها واجهة برمجة التطبيقات (API)، تحقق مما إذا كانت الصفحة تتحديث تلقائيًا بعد انتهاء صلاحية revalidate.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%