كن مطوّر واجهات أمامية محترفا
9 دقيقة قراءة

أساسيات Next.js: من الصفر إلى الاحتراف

مقدّمة عملية لـ Next.js وموجّه التطبيق (App Router) مع TypeScript — ما الذي يضيفه إطار React فوق React، والتوجيه القائم على الملفّات بالمجلّدات وpage.tsx، والتخطيطات والمسارات المتداخلة، ومكوّنات الخادم مقابل العميل، وجلب البيانات على الخادم، واستراتيجيات العرض (SSG/SSR/ISR)، والتنقّل بـ next/link وuseRouter، والملفّات الخاصّة (loading وerror وnot-found)، وواجهة Metadata لتحسين محرّكات البحث، ونظرة أولى على أفعال الخادم وnext/image — مع أخطاء شائعة وتمارين عملية.

يمنحك React المكوّنات؛ ويمنحك Next.js الإطار حولها — التوجيه، والعرض على الخادم، وجلب البيانات، وكومةٌ من تحسينات الأداء — كي تُطلِق تطبيقًا حقيقيًّا بدل تجميع السباكة بنفسك. هذه التدوينة جولةٌ عملية في Next.js وموجّه التطبيق (App Router)، البنية الحديثة المبنيّة على مكوّنات خادم React. تبني على كل ما في السلسلة — أساسيات React، والحالة والخطّافات، والتوجيه، وجلب البيانات — وتُظهِر كيف يعيد Next.js تشكيل كلٍّ منها. كل شيءٍ بـ TypeScript. (هذا الموقع مبنيّ بموجّه التطبيق، فأنت تقرأ خرجًا من هذه المنظومة بالضبط.)

التحوّل الذهنيّ الذي يفتح موجّه التطبيق: تُعرَض المكوّنات على الخادم افتراضيًّا، ولا يُرسَل إلى المتصفّح إلا الأجزاء التفاعلية. مكوّن الخادم يعمل على الخادم، يستطيع جلب البيانات مباشرةً، ويُرسِل HTML جاهزًا — بلا JavaScript. تختار الدخول إلى العميل بـ "use client" فقط حيث تحتاج التفاعلية (حالة، آثار، أحداث). أصِب هذا الحدّ فيتوقّف Next.js عن الشعور بالسحر.

ما الذي يضيفه Next.js إلى React

React العادية (مع Vite) مكتبةٌ على جانب العميل: ينزّل المتصفّح كودك، ثم يعرض. أما Next.js فـإطارٌ يضيف ما يحتاجه كل تطبيق إنتاجيّ:

  • توجيهٌ قائم على الملفّات — المجلّدات تصير عناوين، بلا تهيئة موجّه.
  • العرض على الخادم — تُعرَض الصفحات إلى HTML على الخادم (SSR/SSG) للسرعة وتحسين البحث.
  • مكوّنات الخادم وجلب البيانات — اجلب على الخادم، داخل المكوّن نفسه.
  • تحسينٌ مدمج — تحسين الصور والخطوط والسكربتات؛ وتقسيم الكود تلقائيًّا.

تحصل على تطبيق Next.js بـ:

npx create-next-app@latest my-app

اختَر TypeScript وموجّه التطبيق عند السؤال. المجلّد الأساسيّ هو app/.

التوجيه القائم على الملفّات: المجلّدات مسارات

في موجّه التطبيق، بنية العنوان هي بنية مجلّداتك داخل app/. المجلّد مقطع مسار؛ وملفّ page.tsx فيه يجعل ذلك المقطع صفحة:

app/
  page.tsx            →  /
  about/page.tsx      →  /about
  blog/page.tsx       →  /blog
  blog/[slug]/page.tsx →  /blog/:slug   (مقطع ديناميكيّ)

الصفحة مجرّد مكوّنٍ مُصدَّرٍ افتراضيًّا:

// app/about/page.tsx  →  يُعرَض عند /about
export default function AboutPage() {
  return <h1>About us</h1>;
}

المقاطع الديناميكية تستخدم الأقواس المربّعة، وتصل القيمة كخاصّة. في موجّه التطبيق، params وعدٌ تنتظره:

// app/blog/[slug]/page.tsx  →  /blog/anything
export default async function PostPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  return <h1>Post: {slug}</h1>;
}

لا <Routes>، ولا <Route> — نظام الملفّات هو الموجّه. هذا أكبر فرقٍ منذ اليوم الأول عن React Router.

التخطيطات والمسارات المتداخلة

ملفّ layout.tsx يغلّف كل صفحةٍ في مجلّده (وكل المجلّدات المتداخلة)، عارضًا هيكلًا مشتركًا حول فتحة children — مكافئ <Outlet/> من React Router في موجّه التطبيق. التخطيط الجذريّ مطلوب ويغلّف التطبيق كله:

// app/layout.tsx — يغلّف كل صفحة
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <Nav />
        {children}
        <Footer />
      </body>
    </html>
  );
}

عشّق layout.tsx أعمق (مثلًا app/dashboard/layout.tsx) فيغلّف ذلك القسم فقط، داخل التخطيط الجذريّ. تبقى التخطيطات مركّبةً وأنت تنتقل بين صفحاتها الأبناء، فتصمد حالتها وموضع التمرير.

مكوّنات الخادم مقابل مكوّنات العميل

هذا المفهوم المميِّز لموجّه التطبيق. كل مكوّنٍ مكوّن خادمٍ افتراضيًّا. يعمل على الخادم، ولا يُرسِل كوده إلى المتصفّح أبدًا، ويستطيع فعل أشياء خادميّة بحتة — قراءة قاعدة بيانات، استخدام أسرار، جلب بياناتٍ مباشرة. وتُرسَل النتيجة كـ HTML.

لكن مكوّنات الخادم لا تستطيع استخدام الحالة أو الآثار أو أحداث المتصفّح، لأن تلك تحتاج العميل. حين تحتاج التفاعلية، أضِف توجيه "use client" أعلى الملفّ:

"use client"; // هذا المكوّن (ومستورداته) يعمل في المتصفّح

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0); // الخطّافات تتطلّب مكوّن عميل
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

القاعدة العملية:

  • مكوّن خادم (افتراضيّ) — جلب البيانات، القراءة من خلفيّة، عرض محتوًى ساكن، إبقاء الأسرار والاعتماديات الكبيرة خارج العميل.
  • مكوّن عميل ("use client") — أيّ شيءٍ بـ useState/useEffect، أو معالِجات أحداث، أو واجهات متصفّح.

أبقِ مكوّنات العميل صغيرة وادفعها إلى أوراق الشجرة. نمطٌ شائع: مكوّن خادمٍ يجلب البيانات ويمرّرها كخصائص لمكوّن عميلٍ تفاعليٍّ صغير. لا تعلّم الصفحة كلها "use client" — فقط الزرّ الذي يحتاجه.

جلب البيانات على الخادم

لأن مكوّن الخادم يعمل على الخادم، تجلب البيانات بجعل المكوّن async وawait مباشرةً — بلا useEffect، بلا حالة تحميل، بلا useFetch. تكون البيانات جاهزةً قبل إرسال HTML:

// مكوّن خادم — الجلب يعمل على الخادم، وقت العرض
export default async function ProductsPage() {
  const res = await fetch("https://api.example.com/products");
  const products = (await res.json()) as Product[];

  return (
    <ul>
      {products.map((p) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
}

قارِن هذا بتدوينة جلب البيانات على العميل: على الخادم لا شلّال طلباتٍ في المتصفّح، ولا مؤشّر تحميلٍ للبيانات الأولية، ولا تسرّب أسرار — يحدث الجلب قبل أن تصل الصفحة إلى المستخدم. كما يزيل Next.js تكرار استدعاءات fetch المتطابقة داخل العرض تلقائيًّا، والتخزين اختياريّ (في Next.js 15+ لا يُخزَّن fetch افتراضيًّا) — تتحكّم فيه بـ { cache: "force-cache" } أو { next: { revalidate: 60 } } (أعِد الجلب كل 60 ثانيةً على الأكثر).

استراتيجيات العرض: SSG وSSR وISR

يقرّر Next.js متى تُعرَض الصفحة، وهذا يتحكّم في سرعتها وطزاجتها:

  • ساكن (SSG) — يُعرَض مرّةً وقت البناء إلى HTML. أسرع ما يمكن؛ مثاليّ للمحتوى نادر التغيّر (صفحات التسويق، تدوينات المدوّنة). الافتراضيّ حين لا بيانات ديناميكية للصفحة.
  • معروض على الخادم (SSR) — يُعرَض في كل طلب. استخدمه للبيانات لكل طلبٍ أو لكل مستخدم (cache: "no-store" أو واجهات ديناميكية تُدخِل المسار إلى هذا).
  • تدريجيّ (ISR) — ساكن، لكن يُعاد توليده في الخلفية على فترة (revalidate). تحصل على سرعة الساكن مع طزاجةٍ دورية.

غالبًا لا تختار هذه براية تهيئة — بل تحدّد خيارات جلب بياناتك (هل الجلب مخزّن، هل تقرأ بياناتٍ خاصّة بالطلب) الاستراتيجية. النموذج الذهني: ساكن افتراضيًّا، ديناميكيّ حين تطلب بياناتٍ طازجة أو خاصّة بالطلب.

نقّل بـ next/link — كـ Link في React Router، يقوم بتنقّلٍ على جانب العميل ويُحمّل الهدف مسبقًا لانتقالاتٍ فورية:

import Link from "next/link";

<Link href="/about">About</Link>
<Link href={`/blog/${post.slug}`}>{post.title}</Link>

للتنقّل البرمجيّ (بعد إرسال نموذج مثلًا)، استخدم useRouter — لكن انتبه أنه يأتي من next/navigation ويعمل فقط في مكوّن عميل:

"use client";
import { useRouter } from "next/navigation";

export default function BackButton() {
  const router = useRouter();
  return <button onClick={() => router.back()}>Go back</button>;
}

الملفّات الخاصّة: loading وerror وnot-found

يمنحك موجّه التطبيق اصطلاحات ملفّاتٍ لحالات الواجهة الشائعة — ضعها في مجلّد المسار فيوصلها Next.js:

app/blog/
  page.tsx        → الصفحة
  loading.tsx     → يُعرَض فورًا بينما تُحمَّل بيانات الصفحة (Suspense)
  error.tsx       → يُعرَض إن رمت الصفحة (يجب أن يكون مكوّن عميل)
  not-found.tsx   → يُعرَض لـ notFound() أو المسارات الديناميكية غير المطابقة
// app/blog/loading.tsx — حالة تحميلٍ فورية، بلا إدارة حالة
export default function Loading() {
  return <Spinner />;
}

يستخدم loading.tsx React Suspense تحت الغطاء: يُعرَض التخطيط فورًا وتتدفّق الصفحة حين تجهز بياناتها — بلا راية تحميلٍ يدوية. وerror.tsx حدٌّ يلتقط أخطاء العرض في ذلك المقطع.

تحسين البحث بواجهة Metadata

يتولّى Next.js <head> لك عبر واجهة Metadata — صدّر كائن metadata (أو دالّة generateMetadata للصفحات الديناميكية) فيعرض Next الوسوم، بلا حاجة لـ react-helmet:

import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "About — My Site",
  description: "Who we are and what we build.",
};

// الصفحات الديناميكية تحسبها من المعاملات:
export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>;
}): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPost(slug);
  return { title: post.title, description: post.excerpt };
}

العرض على الخادم مع بياناتٍ وصفية حقيقية هو سبب كون صفحات Next.js صديقةً لمحرّكات البحث جاهزةً — تحصل الزواحف على HTML جاهزٍ بعناوين وأوصافٍ صحيحة.

نظرة أولى: أفعال الخادم وnext/image

شيئان آخران ستقابلهما مبكّرًا:

  • أفعال الخادم (Server Actions) تتيح لنموذجٍ استدعاء كود الخادم مباشرةً، بلا بناء مسار API. دالّةٌ مُعلَّمة بـ "use server" تعمل على الخادم عند إرسال النموذج — الطريقة الحديثة لمعالجة التعديلات.
  • next/image يستبدل <img> بتحجيمٍ تلقائيّ، وتحميلٍ كسول، وصيغٍ حديثة، لمكسبٍ كبير في الأداء ومؤشّرات الويب الأساسية.
import Image from "next/image";

<Image src="/hero.jpg" alt="" width={1200} height={600} priority />;

يستحقّ هذان أن تعرف بوجودهما الآن وأن تتعمّق فيهما وأنت تبني.

الأخطاء الشائعة

  • وضع "use client" أعلى الصفحة كلها — تُرسِل كل شيءٍ إلى المتصفّح وتخسر العرض على الخادم. علّم المكوّنات التفاعلية الصغيرة فقط؛ وأبقِ الصفحات مكوّنات خادم.
  • استخدام الخطّافات في مكوّن خادمuseState/useEffect ترمي بلا "use client". التفاعلية تحتاج مكوّن عميل.
  • استخدام <a href> للروابط الداخلية — إعادة تحميلٍ كاملة، بلا تحميلٍ مسبق. استخدم next/link.
  • استيراد useRouter من next/router — هذا موجّه الصفحات القديم. في موجّه التطبيق هو next/navigation.
  • نسيان أن params/searchParams وعود — في موجّه التطبيق عليك await لها.
  • الجلب على العميل حين يكفي الخادم — نقل جلب البيانات الأولية إلى useEffect يعيد المؤشّرات والشلّالات التي يتجنّبها Next.js. اجلب في مكوّن الخادم حين تستطيع.
  • تسريب أسرارٍ إلى مكوّنات العميل — أيّ شيءٍ في ملفّ "use client" (ومستورداته) يصل إلى المتصفّح. أبقِ مفاتيح API في مكوّنات الخادم / الكود الخادميّ.

تمارين

جرّب كلًّا قبل فتح الحلّ.

تمرين 1 — اصنع مسارًا

أيّ ملفٍّ تنشئ كي يعرض العنوان /pricing مكوّن PricingPage؟

اعرض الحل

app/pricing/page.tsx، بتصدير المكوّن افتراضيًّا:

export default function PricingPage() {
  return <h1>Pricing</h1>;
}

المجلّد تحت app/ هو مقطع المسار؛ وpage.tsx يجعله صفحةً قابلة للتنقّل.

تمرين 2 — خادم أم عميل؟

مكوّنٌ يعرض عدّادًا يتحدّث حيًّا بزرّ. مكوّن خادمٍ أم عميل، وما الذي يجعله كذلك؟

اعرض الحل

مكوّن عميل — يحتاج useState ومعالِج onClick، وكلاهما يتطلّب المتصفّح. أضِف "use client" أعلى الملفّ. (أبقِه صغيرًا؛ ويستطيع مكوّن خادمٍ أبٌ العرض حوله.)

تمرين 3 — اجلب في مكوّن خادم

اكتب صفحة مكوّن خادمٍ تجلب /api/posts وتسرد العناوين — بلا useEffect.

اعرض الحل
export default async function PostsPage() {
  const res = await fetch("https://example.com/api/posts");
  const posts = (await res.json()) as Post[];
  return (
    <ul>
      {posts.map((p) => (
        <li key={p.id}>{p.title}</li>
      ))}
    </ul>
  );
}

المكوّن async وينتظر الجلب مباشرةً؛ فتكون البيانات جاهزةً قبل إرسال HTML، فلا حالة تحميلٍ تديرها.

تمرين 4 — بياناتٌ وصفية ديناميكية

لـ app/blog/[slug]/page.tsx، كيف تضبط <title> الصفحة من عنوان التدوينة؟

اعرض الحل
export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>;
}): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPost(slug);
  return { title: post.title };
}

صدّر generateMetadata (لا كائن metadata الساكن) حين تعتمد الوسوم على معاملات المسار، فيعرض Next الـ <head> لك.

النموذج الذهني الذي تحتفظ به

Next.js هو React زائد الإطار حولها: نظام الملفّات هو الموجّه (مجلّدات app/…/page.tsx تصير عناوين، وlayout.tsx يغلّفها كـ <Outlet/>)، والمكوّنات مكوّنات خادمٍ افتراضيًّا — تعمل على الخادم، وتجلب البيانات مباشرةً بـ async/await، وتُبقي الأسرار والكود الثقيل خارج العميل، وتُرسِل HTML جاهزًا. تختار الدخول إلى المتصفّح بـ "use client" فقط للأوراق التفاعلية التي تحتاج حالةً أو آثارًا أو أحداثًا — أبقِ ذلك الحدّ صغيرًا. ينتقل جلب البيانات إلى الخادم (بلا مؤشّراتٍ أو شلّالاتٍ للبيانات الأولية؛ يخزّنها Next ويعيد التحقّق منها)، وهذا أيضًا يقرّر استراتيجية عرضك — ساكنٌ افتراضيًّا، ديناميكيّ حين تطلب بياناتٍ طازجة أو لكل طلب. نقّل بـ next/link، والجأ إلى الملفّات الخاصّة (loading وerror وnot-found) بدل كتابة تلك الحالات يدويًّا، واحصل على تحسين البحث مجّانًا عبر واجهة Metadata. أمسِك بحدّ "خادمٌ افتراضيًّا، عميلٌ عن قصد"، فينضبط موجّه التطبيق — ونموذج الواجهة = دالّة(الحالة) كاملًا ممتدًّا عبر فاصل الخادم/العميل — في مكانه.