Next.js: React مع خادم وتوجيه
درس 6 من 6
6▲ Next.js: React مع خادم وتوجيه
الصورة الكبيرةReact وحده يرسم في المتصفح: تصل صفحة فارغة، ثم يُحمَّل JavaScript، ثم يجلب البيانات، ثم تمتلئ. هذا بطيء على الجوال، ومحركات البحث قد ترى صفحة فارغة. Next.js يرسم على الخادم أولًا ويرسل HTML جاهزًا بالمحتوى، ويضيف التوجيه والبيانات والـ API في مشروع واحد. ومنصة درب نفسها مبنية به.
ماذا تشاهد في الرسم: الصفحة تُبنى على الخادم بالبيانات، تصل جاهزة للقراءة والبحث، ثم تُفعَّل الأجزاء التفاعلية فقط في المتصفح.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
React يرسل لك أثاثًا مفكّكًا مع كتيّب، وتركّبه في بيتك (المتصفح). Next.js يرسل غرفة مركّبة جاهزة، وأنت فقط تشغّل الأجهزة التي تحتاج كهرباء (الأزرار والنماذج). لهذا تظهر الصفحة أسرع وتقرؤها محركات البحث. ومشروع Next.js مرتّب كخزانة ملفات: كل مجلد فيها صفحة من موقعك، باسمه في الرابط.
App Router: كل مجلد تحت app/ مقطع مسار، وpage.tsx يجعله قابلًا للزيارة، وlayout.tsx يغلّف الأبناء ويحتفظ بحالته بين التنقّلات، وloading.tsx وerror.tsx حدود Suspense وError Boundary تلقائية، و[slug] مقطع ديناميكي. المكوّنات خادمية افتراضيًا (React Server Components): تعمل غير متزامنة وتصل لقاعدة البيانات مباشرة ولا تُرسل كودها للمتصفح، ونتيجتها تُبثّ (streaming) كـ RSC payload. "use client" يصنع حدًّا تُرسل بعده المكوّنات للعميل وتُرطَّب (hydration)، ولا تُمرَّر عبره إلا قيم قابلة للتسلسل. التخزين المؤقت وإعادة التحقّق (revalidate، revalidatePath) تحدّد التصيير الثابت (SSG/ISR) أو الديناميكي، وgenerateStaticParams يبني المسارات الديناميكية مسبقًا، وgenerateMetadata يولّد وسوم head. route.ts يعرّف معالجات HTTP، وServer Actions ("use server") دوال خادم تُنادى من نموذج مباشرة.
قبل أن تبدأ
⚛️React
تعرف المكوّن وprops وuseState وuseEffect.
📨HTTP
تعرف الأفعال GET وPOST ورموز الحالة.
🟩Node.js وExpress
تعرف ما الخادم وكيف يرد على طلب.
الفكرة الجوهرية
1لماذا التصيير على الخادم؟
في تطبيق React عادي (تصيير العميل، CSR) يرسل الخادم HTML فيه <div id="root"></div> فارغ فقط، ثم المتصفح يحمّل JavaScript ويشغّله ويجلب البيانات ويرسم. في التصيير على الخادم (SSR) يشغّل الخادم المكوّنات أولًا ويرسل HTML فيه المحتوى كاملًا، ثم يُحمَّل JavaScript ليجعل الأزرار تعمل. المثال يحاكي ما يصل للمتصفح في الحالتين:
const menu = [{ name: "Latte", price: 14 }, { name: "Mocha", price: 16 }];
const Menu = (items) => `<ul>${items.map((d) => `<li>${d.name} ${d.price} SAR</li>`).join("")}</ul>`;
// client-side rendering: the first response is an empty shell
const csrResponse = `<body><div id="root"></div><script src="/app.js"></script></body>`;
// server-side rendering: the server already ran the component
const ssrResponse = `<body><div id="root">${Menu(menu)}</div><script src="/app.js"></script></body>`;
const words = (html) => html.replace(/<[^>]+>/g, " ").trim().split(/\s+/).filter(Boolean).length;
console.log("CSR: words a search engine sees first:", words(csrResponse));
console.log("SSR: words a search engine sees first:", words(ssrResponse));
console.log(ssrResponse);النتيجة: مع SSR يرى المستخدم المحتوى أسرع (خصوصًا على جوال بطيء)، ومحرك البحث يقرأ النص مباشرة، ومعاينة الرابط في واتساب تعمل. Next.js يفعل هذا تلقائيًا.
2إنشاء المشروع وهيكله
npx create-next-app@latest cafe # accept the defaults: TypeScript, App Router cd cafe npm run dev # open http://localhost:3000
لا ملف إعدادات للمسارات: المجلد هو المسار. كل مجلد داخل app/ يصير جزءًا من الرابط، والملفات بأسماء خاصة داخله لها أدوار محدّدة:
app/
layout.tsx -> wraps every page (header, footer); has <html> and <body>
page.tsx -> /
loading.tsx -> shown while a page in this folder loads
error.tsx -> shown if a page in this folder throws
not-found.tsx -> the 404 page
menu/
page.tsx -> /menu
[slug]/
page.tsx -> /menu/latte, /menu/mocha ...
api/
orders/
route.ts -> GET/POST /api/orders
public/
logo.svg -> served as /logo.svg3التخطيطات والروابط
layout.tsx يغلّف كل الصفحات تحته بعناصر مشتركة (الشريط العلوي، القائمة، الذيل)، ويبقى بلا إعادة رسم حين تتنقّل بين صفحاته، فلا يومض الشريط. و<Link href="/menu"> من next/link بدل <a> العادي للتنقّل داخل موقعك: يجلب الصفحة التالية مسبقًا وينتقل بلا إعادة تحميل كاملة.
// app/layout.tsx
import Link from "next/link";
import "./globals.css";
export const metadata = { title: "Darb Café", description: "Coffee roasted every morning" };
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ar" dir="rtl">
<body>
<header>
<Link href="/">☕ Darb</Link>
<nav><Link href="/menu">Menu</Link> <Link href="/about">About</Link></nav>
</header>
<main>{children}</main>
<footer>© 2026</footer>
</body>
</html>
);
}4مكوّنات الخادم والعميل
أهم فكرة في Next.js الحديث: كل مكوّن خادمي افتراضيًا. المكوّن الخادمي يستطيع أن يكون async، ويقرأ من قاعدة البيانات أو API مباشرة، وكوده لا يصل للمتصفح أبدًا (فالمفاتيح السرية آمنة فيه، والصفحة أخف). لكنه لا يملك حالة ولا أحداثًا. المكوّن الذي يحتاج useState أو onClick أو useEffect يبدأ بسطر "use client".
// app/menu/page.tsx (server component: no "use client")
import { AddButton } from "./AddButton";
export default async function MenuPage() {
const res = await fetch("https://api.example.com/menu", { next: { revalidate: 60 } });
const items: { slug: string; name: string; price: number }[] = await res.json();
return (
<ul>
{items.map((i) => (
<li key={i.slug}>{i.name} · {i.price} SAR <AddButton slug={i.slug} /></li>
))}
</ul>
);
}// app/menu/AddButton.tsx
"use client";
import { useState } from "react";
export function AddButton({ slug }: { slug: string }) {
const [added, setAdded] = useState(false);
return <button onClick={() => setAdded(true)}>{added ? "Added ✓" : "Add"}</button>;
}revalidate: 60 يعني: احفظ نتيجة الجلب وأعد استخدامها، وجدّدها كل 60 ثانية على الأكثر. فالصفحة سريعة كأنها ثابتة، ومحدَّثة بما يكفي.
"use client" في أعلى كل ملف «احتياطًا». تخسر التصيير على الخادم وتُرسل كودًا أكثر للمتصفح. اجعل حدّ العميل أصغر ما يمكن: الزر فقط لا الصفحة.5صفحات التحميل والخطأ
تذكّر حالات الواجهة الأربع من درس غير المتزامن. Next.js يجعل اثنتين منها ملفات: loading.tsx يظهر فورًا أثناء جلب بيانات الصفحة، وerror.tsx (مكوّن عميل) يظهر إن رمت الصفحة خطأ، مع دالة reset للمحاولة مجددًا. لا تكتب if (loading) بيدك في كل صفحة.
// app/menu/loading.tsx
export default function Loading() {
return <p>⏳ Loading the menu...</p>;
}
// app/menu/error.tsx
"use client";
export default function MenuError({ error, reset }: { error: Error; reset: () => void }) {
return (
<div role="alert">
<p>Could not load the menu.</p>
<button onClick={reset}>Try again</button>
</div>
);
}6المسارات الديناميكية وبيانات الصفحة
مجلد اسمه بين أقواس مربّعة [slug] يلتقط أي قيمة في ذلك الجزء من الرابط، وتصل للصفحة كـ params. إن لم تجد العنصر نادِ notFound() فيظهر 404 صحيح. وgenerateMetadata يولّد عنوان كل صفحة ووصفها حسب بياناتها، فيظهر «لاتيه | مقهى درب» في نتائج جوجل.
7Route Handlers وServer Actions: الخادم في نفس المشروع
Route Handler: ملف route.ts يصدّر دوالًا باسم الفعل (GET، POST) وتعيد Response، مثل مسار Express صغير. مفيد لـ webhook أو API يستهلكه تطبيق آخر. Server Action: دالة تبدأ بـ "use server" تعمل على الخادم، وتمرّرها لخاصية action في نموذج مباشرة، فيرسل النموذج بياناته لها بلا fetch ولا API تكتبه بيدك. وللخدمات الكبيرة يبقى خادم مستقل (مثل خادم درب) أوضح.
// app/api/orders/route.ts
export async function POST(request: Request) {
const body = await request.json();
if (!body.item) return Response.json({ error: "item is required" }, { status: 400 });
// save the order ...
return Response.json({ id: 42, ...body }, { status: 201 });
}// app/contact/page.tsx
import { revalidatePath } from "next/cache";
async function sendMessage(formData: FormData) {
"use server"; // runs on the server only
const text = String(formData.get("text") ?? "").trim();
if (!text) return;
// await db.messages.insert({ text }); <- direct database access is safe here
revalidatePath("/contact");
}
export default function Contact() {
return (
<form action={sendMessage}>
<label htmlFor="text">Your message</label>
<textarea id="text" name="text" required />
<button>Send</button>
</form>
);
}8النشر: من جهازك إلى رابط حقيقي
npm run build يبني نسخة الإنتاج ويخبرك أي صفحة ثابتة (○) وأي ديناميكية (ƒ). وللنشر: ارفع المشروع إلى GitHub، ثم اربطه بـ Vercel (الشركة المطوّرة لـ Next.js) أو أي منصة تدعم Node، فيُنشر مع كل git push تلقائيًا. أو ابنِ له صورة Docker كما ستتعلّم في مسار النشر.
apps/web/src/app/[lang] مشروع Next.js حقيقي. تتبّع صفحة درس من المجلد l/[slug] إلى الطلب الذي يجلب بياناته، وستفهم كل مفهوم في هذا الدرس في كود يعمل أمامك الآن.مثال محلول
المسألة: صفحة لكل مشروب /menu/latte، تظهر 404 لمشروب غير موجود، وتُبنى مسبقًا، وعنوانها في جوجل اسم المشروب. الحل: generateStaticParams يبني الصفحات وقت البناء، وgenerateMetadata يولّد العنوان، وnotFound() للمفقود:
// app/menu/[slug]/page.tsx
import { notFound } from "next/navigation";
const DRINKS = {
latte: { name: "Latte", price: 14, about: "Silky milk over a double espresso." },
mocha: { name: "Mocha", price: 16, about: "Espresso, chocolate and milk." },
};
type Props = { params: Promise<{ slug: string }> };
export function generateStaticParams() {
return Object.keys(DRINKS).map((slug) => ({ slug }));
}
export async function generateMetadata({ params }: Props) {
const { slug } = await params;
const drink = DRINKS[slug as keyof typeof DRINKS];
return { title: drink ? `${drink.name} | Darb Café` : "Not found" };
}
export default async function Drink({ params }: Props) {
const { slug } = await params;
const drink = DRINKS[slug as keyof typeof DRINKS];
if (!drink) notFound();
return (
<article>
<h1>{drink.name} · {drink.price} SAR</h1>
<p>{drink.about}</p>
</article>
);
}خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
| Next.js بديل لـ React. | هو مبني على React ويضيف الخادم والتوجيه والبناء. |
| كل مكوّن في Next.js يعمل في المتصفح. | في App Router الافتراضي خادمي، ولا يصل للمتصفح إلا ما بعد "use client". |
| Next.js يعمل على Vercel فقط. | يعمل على أي خادم Node أو في حاوية Docker، ودرب مثال على ذلك. |
أين تقابله في العمل
🛍️المتاجر والمدوّنات
صفحات سريعة تقرؤها محركات البحث مهمة للمبيعات.
📊لوحات التحكّم
مكوّنات خادم تقرأ القاعدة مباشرة بلا API وسيط.
🎓منصة درب
الواجهة التي تقرأ منها هذا الدرس مكتوبة بـ Next.js.
اختبر فهمك
أي ملف يجعل المسار /about قابلًا للزيارة؟
في App Router المجلد هو المقطع وpage.tsx يجعله صفحة.
مكوّن يقرأ قاعدة البيانات بمفتاح سرّي. أين يجب أن يكون؟
كود المكوّن الخادمي لا يُرسل للمتصفح، فالسر لا يتسرّب.
متى تحتاج "use client"؟
الحالة والأحداث تحتاج المتصفح، والباقي يبقى على الخادم.
لماذا التصيير على الخادم أفضل لمحركات البحث؟
الزاحف يقرأ النص مباشرة دون أن يشغّل JavaScript.
أي ملف يظهر تلقائيًا أثناء جلب بيانات صفحة في نفس المجلد؟
loading.tsx حدّ Suspense تلقائي لصفحات مجلده.
بطاقات مراجعة سريعة
تحقق من فهمك
لماذا التصيير على الخادم أسرع على الجوال؟
لأن المحتوى يصل في أول رد، فلا ينتظر الجوال تحميل JavaScript وتشغيله وجلب البيانات قبل أن يرى شيئًا.
ما فائدة layout.tsx؟
يغلّف كل الصفحات تحته بعناصر مشتركة (رأس، قائمة) ويبقى بلا إعادة رسم عند التنقّل.
كيف تُظهر 404 لصفحة ديناميكية لا عنصر لها؟
بمناداة notFound() من next/navigation.
هل يجب التحقّق من مدخلات Server Action؟
نعم، فهي نقطة دخول للخادم يمكن مناداتها بطلب مصنوع خارج نموذجك.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
توثيق رسمي3
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.