Next.js

Server Components في Next.js — دليل شامل 2026

📅 2026-11-14⏱ 9 دقائق قراءة
في المقال السابق، تعلمت App Router. الآن سنتعمق في **Server Components** — وهي **أكبر ميزة** في Next.js 15، وتغيّر طريقة تفكيرك في بناء التطبيقات. في هذا الدليل العملي، سنأخذك خطوة بخطوة لفهم Server Components، مع تمارين وحلول. ## المشكلة التي تحلها Server Components **قبل Server Components:** ```tsx // كل شيء في المتصفح function UserProfile() { const [user, setUser] = useState(null); useEffect(() => { fetch("/api/user") .then((res) => res.json()) .then(setUser); }, []); if (!user) return <div>جاري التحميل...</div>; return <h1>{user.name}</h1>; } ``` **المشاكل:** - ❌ الكود يُرسل للمتصفح. - ❌ 3 مراحل (تحميل، fetch، عرض). - ❌ بيانات لا تظهر لـ SEO. - ❌ Bundle كبير. **بعد Server Components:** ```tsx // يعمل على السيرفر async function UserProfile() { const user = await getUser(); // مباشرة من DB return <h1>{user.name}</h1>; } ``` **الحلول:** - ✅ الكود لا يُرسل للمتصفح. - ✅ مرحلة واحدة. - ✅ بيانات لـ SEO. - ✅ Bundle أصغر. ## الفرق الأساسي <table> <thead> <tr> <th>المعيار</th> <th>Server Component</th> <th>Client Component</th> </tr> </thead> <tbody> <tr> <td><strong>الافتراضي</strong></td> <td>✅ نعم</td> <td>❌ يحتاج "use client"</td> </tr> <tr> <td><strong>useState</strong></td> <td>❌</td> <td>✅</td> </tr> <tr> <td><strong>useEffect</strong></td> <td>❌</td> <td>✅</td> </tr> <tr> <td><strong>onClick</strong></td> <td>❌</td> <td>✅</td> </tr> <tr> <td><strong>async/await</strong></td> <td>✅ مباشرة</td> <td>❌ (في useEffect)</td> </tr> <tr> <td><strong>الوصول لقاعدة البيانات</strong></td> <td>✅</td> <td>❌</td> </tr> <tr> <td><strong>الوصول للملفات</strong></td> <td>✅</td> <td>❌</td> </tr> <tr> <td><strong>البيانات الحساسة</strong></td> <td>✅ آمن</td> <td>❌ مكشوف</td> </tr> <tr> <td><strong>Bundle Size</strong></td> <td>✅ صفر</td> <td>⚠️ يزيد</td> </tr> </tbody> </table> ## Server Components **افتراضياً، كل مكون في `app/` هو Server Component.** ### 1. جلب بيانات من API ```tsx // app/users/page.tsx async function getUsers() { const res = await fetch("https://jsonplaceholder.typicode.com/users", { cache: "no-store", // ← لا تخزين }); return res.json(); } export default async function UsersPage() { const users = await getUsers(); return ( <ul> {users.map((user) => ( <li key={user.id}>{user.name}</li> ))} </ul> ); } ``` **⚠️ لاحظ:** - `async` مباشرة. - `await` في المكوّن. - لا `useState` ولا `useEffect`. ### 2. الوصول لقاعدة البيانات ```tsx // app/posts/page.tsx import { prisma } from "@/lib/prisma"; export default async function PostsPage() { const posts = await prisma.post.findMany(); return ( <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> ); } ``` **الفائدة:** لا تحتاج API وسيط — **مباشرة من DB**. ### 3. الوصول للملفات ```tsx // app/data/page.tsx import fs from "fs/promises"; export default async function DataPage() { const content = await fs.readFile("./data.json", "utf-8"); const data = JSON.parse(content); return <pre>{JSON.stringify(data, null, 2)}</pre>; } ``` ## Client Components **للاستخدام Client Component، أضف `"use client"` في أعلى الملف.** ### 1. استخدام useState ```tsx // app/counter/page.tsx "use client"; import { useState } from "react"; export default function CounterPage() { const [count, setCount] = useState(0); return ( <div> <p>العدد: {count}</p> <button onClick={() => setCount(count + 1)}>+1</button> </div> ); } ``` ### 2. استخدام useEffect ```tsx "use client"; import { useState, useEffect } from "react"; export default function Clock() { const [time, setTime] = useState(new Date()); useEffect(() => { const timer = setInterval(() => setTime(new Date()), 1000); return () => clearInterval(timer); }, []); return <p>{time.toLocaleTimeString("ar-EG")}</p>; } ``` ### 3. استخدام onMouseOver ```tsx "use client"; export default function HoverCard() { return ( <div onMouseOver={() => console.log("Hover!")} className="p-4 bg-blue-100" > مرر الفأرة </div> ); } ``` ## متى تستخدم كل نوع؟ <table> <thead> <tr> <th>الحالة</th> <th>النوع</th> </tr> </thead> <tbody> <tr> <td>عرض بيانات ثابتة</td> <td>Server</td> </tr> <tr> <td>جلب من API/DB</td> <td>Server</td> </tr> <tr> <td>قراءة ملفات</td> <td>Server</td> </tr> <tr> <td>بيانات حساسة (API Keys)</td> <td>Server</td> </tr> <tr> <td>useState / useEffect</td> <td>Client</td> </tr> <tr> <td>onClick / onChange</td> <td>Client</td> </tr> <tr> <td>Browser APIs</td> <td>Client</td> </tr> <tr> <td>Context / Providers</td> <td>Client</td> </tr> </tbody> </table> ## تركيب المكونات ### 1. Server داخل Server ```tsx // app/dashboard/page.tsx import Stats from "./Stats"; import RecentOrders from "./RecentOrders"; export default async function Dashboard() { return ( <div> <Stats /> <RecentOrders /> </div> ); } ``` **✅ يعمل** — Server داخل Server. ### 2. Client داخل Server ```tsx // app/page.tsx import LikeButton from "./LikeButton"; // ← "use client" export default function Home() { return ( <div> <h1>مقالي</h1> <LikeButton /> </div> ); } ``` **✅ يعمل** — Client داخل Server. ### 3. Server داخل Client (مشكلة!) ```tsx // ❌ خطأ: "use client"; import ServerComponent from "./ServerComponent"; export default function ClientWrapper() { return <ServerComponent />; // ← لن يعمل } ``` **❌ لا يعمل** — Server داخل Client. ### 4. الحل: تمرير Server كـ children ```tsx // ✅ صحيح: "use client"; export default function ClientWrapper({ children, }: { children: React.ReactNode; }) { return <div>{children}</div>; } ``` ```tsx // app/page.tsx (Server) import ClientWrapper from "./ClientWrapper"; import ServerComponent from "./ServerComponent"; export default function Page() { return ( <ClientWrapper> <ServerComponent /> </ClientWrapper> ); } ``` **✅ يعمل!** — Server يُمرَّر كـ `children` لـ Client. ## مثال عملي: صفحة منتج ### 1. Server Component (جلب البيانات) **`app/products/[id]/page.tsx`:** ```tsx import AddToCartButton from "./AddToCartButton"; async function getProduct(id: string) { const res = await fetch(`https://api.example.com/products/${id}`); return res.json(); } export default async function ProductPage({ params, }: { params: Promise<{ id: string }>; }) { const { id } = await params; const product = await getProduct(id); return ( <div className="max-w-4xl mx-auto p-8"> <img src={product.image} alt={product.name} className="w-full h-96 object-cover rounded-xl" /> <h1 className="text-3xl font-bold mt-6">{product.name}</h1> <p className="text-gray-600 mt-4">{product.description}</p> <p className="text-2xl font-bold text-blue-600 mt-4"> {product.price} $ </p> <AddToCartButton productId={product.id} /> </div> ); } ``` ### 2. Client Component (زر الإضافة) **`app/products/[id]/AddToCartButton.tsx`:** ```tsx "use client"; import { useState } from "react"; export default function AddToCartButton({ productId, }: { productId: string; }) { const [added, setAdded] = useState(false); const handleClick = () => { // ... منطق إضافة للسلة setAdded(true); setTimeout(() => setAdded(false), 2000); }; return ( <button onClick={handleClick} disabled={added} className="mt-6 bg-blue-600 text-white px-8 py-3 rounded-lg font-bold hover:bg-blue-700 disabled:bg-green-600 transition" > {added ? "✅ تمت الإضافة" : "🛒 أضف للسلة"} </button> ); } ``` **الفائدة:** الصفحة Server (سريعة، SEO)، والزر Client (تفاعلي). ## Streaming و Suspense **Suspense** تسمح بتحميل أجزاء الصفحة **بالتدريج**. ```tsx import { Suspense } from "react"; export default function Dashboard() { return ( <div> <h1>لوحة التحكم</h1> <Suspense fallback={<p>جاري تحميل الإحصائيات...</p>}> <Stats /> </Suspense> <Suspense fallback={<p>جاري تحميل الطلبات...</p>}> <RecentOrders /> </Suspense> </div> ); } ``` **الفائدة:** الصفحة تبدأ بالظهور، والأجزاء البطيئة تُحمّل لاحقاً. ## cache في fetch ```tsx // تخزين دائم (Static) fetch(url, { cache: "force-cache" }); // افتراضي // لا تخزين (Dynamic) fetch(url, { cache: "no-store" }); // إعادة التحقق كل 60 ثانية (ISR) fetch(url, { next: { revalidate: 60 } }); ``` <table> <thead> <tr> <th>الإعداد</th> <th>السلوك</th> </tr> </thead> <tbody> <tr> <td><code>force-cache</code></td> <td>تخزين دائم</td> </tr> <tr> <td><code>no-store</code></td> <td>لا تخزين (دائماً جديد)</td> </tr> <tr> <td><code>revalidate: 60</code></td> <td>إعادة التحقق كل 60 ثانية</td> </tr> </tbody> </table> ## تمارين عملية ### تمرين 1: Server Component أنشئ صفحة تعرض قائمة مستخدمين. **الحل:** ```tsx async function getUsers() { const res = await fetch("https://jsonplaceholder.typicode.com/users"); return res.json(); } export default async function UsersPage() { const users = await getUsers(); return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>; } ``` ### تمرين 2: Client Component أنشئ زر تبديل الوضع الداكن. **الحل:** ```tsx "use client"; import { useState } from "react"; export default function ThemeToggle() { const [dark, setDark] = useState(false); return ( <button onClick={() => setDark(!dark)}> {dark ? "☀️" : "🌙"} </button> ); } ``` ### تمرين 3: مزج Server + Client أنشئ صفحة مقالات مع زر "Like". **الحل:** ```tsx // Server async function getPosts() { // ... } export default async function Posts() { const posts = await getPosts(); return ( <ul> {posts.map((p) => ( <li key={p.id}> {p.title} <LikeButton postId={p.id} /> </li> ))} </ul> ); } ``` ### تمرين 4: Suspense أضف Suspense لصفحة. **الحل:** ```tsx import { Suspense } from "react"; <Suspense fallback={<p>جاري التحميل...</p>}> <SlowComponent /> </Suspense> ``` ### تمرين 5: cache جرّب cache: "no-store". **الحل:** ```tsx const res = await fetch(url, { cache: "no-store" }); ``` ### تمرين 6: Server مع DB اجلب بيانات من ملف. **الحل:** ```tsx import fs from "fs/promises"; export default async function DataPage() { const content = await fs.readFile("./data.json", "utf-8"); return <pre>{content}</pre>; } ``` ### تمرين 7: Client مع useEffect أنشئ ساعة رقمية. **الحل:** ```tsx "use client"; import { useState, useEffect } from "react"; export default function Clock() { const [time, setTime] = useState(new Date()); useEffect(() => { const timer = setInterval(() => setTime(new Date()), 1000); return () => clearInterval(timer); }, []); return <p>{time.toLocaleTimeString("ar-EG")}</p>; } ``` ### تمرين 8: تطبيق مختلط ابنِ تطبيق يستخدم Server و Client معاً. **الحل:** (راجع مثال صفحة المنتج) ## حل المشاكل الشائعة ### 🔴 المشكلة 1: `useState is not defined` **السبب:** استخدمت `useState` في Server Component. **الحل:** أضف `"use client"`. ### 🔴 المشكلة 2: `async/await` في Client **السبب:** Client Component لا يمكن أن يكون `async`. **الحل:** استخدم `useEffect` لجلب البيانات. ### 🔴 المشكلة 3: Server داخل Client **السبب:** حاولت استيراد Server Component في Client Component. **الحل:** مرّره كـ `children`. ### 🔴 المشكلة 4: `window is not defined` **السبب:** استخدمت `window` في Server. **الحل:** استخدم `"use client"` أو `typeof window !== "undefined"`. ### 🔴 المشكلة 5: البيانات لا تُحدّث **السبب:** `cache` يخزّن البيانات. **الحل:** ```tsx fetch(url, { cache: "no-store" }); ``` ## جدول مقارنة سريعة <table> <thead> <tr> <th>الميزة</th> <th>Server</th> <th>Client</th> </tr> </thead> <tbody> <tr> <td>async/await</td> <td>✅</td> <td>❌</td> </tr> <tr> <td>useState</td> <td>❌</td> <td>✅</td> </tr> <tr> <td>useEffect</td> <td>❌</td> <td>✅</td> </tr> <tr> <td>onClick</td> <td>❌</td> <td>✅</td> </tr> <tr> <td>DB Access</td> <td>✅</td> <td>❌</td> </tr> <tr> <td>FS Access</td> <td>✅</td> <td>❌</td> </tr> <tr> <td>Bundle Size</td> <td>0 KB</td> <td>يزيد</td> </tr> </tbody> </table> ## قائمة تحقق نهائية <table> <thead> <tr> <th>المهمة</th> <th>الحالة</th> </tr> </thead> <tbody> <tr> <td>فهم Server vs Client</td> <td>⬜</td> </tr> <tr> <td>Server Component مع fetch</td> <td>⬜</td> </tr> <tr> <td>Client Component مع useState</td> <td>⬜</td> </tr> <tr> <td>مزج Server + Client</td> <td>⬜</td> </tr> <tr> <td>Suspense</td> <td>⬜</td> </tr> <tr> <td>cache في fetch</td> <td>⬜</td> </tr> <tr> <td>حل التمارين الثمانية</td> <td>⬜</td> </tr> </tbody> </table> ## ماذا بعد هذا المقال؟ الآن بعد أن أتقنت Server Components، أنت جاهز للمقال التالي: 1. **Data Fetching** — جلب البيانات المتقدم. 2. **API Routes** — بناء APIs. 3. **Authentication** — المصادقة. ## الخلاصة في هذا المقال، تعلمت: - ✅ ما هي Server Components. - ✅ Server vs Client Components. - ✅ `"use client"`. - ✅ مزج Server + Client. - ✅ Suspense و Streaming. - ✅ cache في fetch. - ✅ حل 8 تمارين عملية. **تذكر:** Server Components هي **الميزة الأكبر** في Next.js 15 — استخدمها بحكمة.