Next.js

جلب البيانات في Next.js — دليل شامل 2026

📅 2026-11-15⏱ 9 دقائق قراءة
في المقال السابق، تعلمت Server Components. الآن سنتعمق في **جلب البيانات (Data Fetching)** — وهو جوهر أي تطبيق ويب حقيقي. في هذا الدليل العملي، سنأخذك خطوة بخطوة لفهم جلب البيانات في Next.js، مع تمارين وحلول. ## طرق جلب البيانات <table> <thead> <tr> <th>الطريقة</th> <th>المكان</th> <th>الاستخدام</th> </tr> </thead> <tbody> <tr> <td><strong>fetch في Server Component</strong></td> <td>السيرفر</td> <td>الأكثر شيوعاً</td> </tr> <tr> <td><strong>Server Actions</strong></td> <td>السيرفر</td> <td>البيانات المتغيرة (POST/PUT)</td> </tr> <tr> <td><strong>Route Handlers</strong></td> <td>السيرفر</td> <td>API Endpoints</td> </tr> <tr> <td><strong>useEffect</strong></td> <td>المتصفح</td> <td>البيانات الديناميكية</td> </tr> <tr> <td><strong>SWR / React Query</strong></td> <td>المتصفح</td> <td>التخزين المؤقت المتقدم</td> </tr> </tbody> </table> ## fetch في Server Component **أسهل طريقة:** استخدام `fetch` مباشرة في 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((user) => ( <li key={user.id}>{user.name}</li> ))} </ul> ); } ``` **الفائدة:** - ✅ يعمل على السيرفر. - ✅ SEO ممتاز. - ✅ Bundle أصغر. - ✅ مباشر — لا `useEffect`. ## cache في fetch Next.js يوسّع `fetch` بـ `cache` و `next.revalidate`. ### 1. التخزين الدائم (Static) ```tsx fetch(url, { cache: "force-cache" }); // ← افتراضي ``` **السلوك:** تُجلب البيانات مرة واحدة، وتُخزّن للأبد. **الاستخدام:** بيانات لا تتغير (مقالات، صفحات ثابتة). ### 2. لا تخزين (Dynamic) ```tsx fetch(url, { cache: "no-store" }); ``` **السلوك:** تُجلب البيانات **في كل طلب**. **الاستخدام:** بيانات حية (أسعار، أخبار). ### 3. إعادة التحقق (ISR) ```tsx fetch(url, { next: { revalidate: 60 } }); // ← كل 60 ثانية ``` **السلوك:** تُجلب البيانات، وتُخزّن، وتُحدَّث كل 60 ثانية. **الاستخدام:** بيانات تتغير بشكل دوري. <table> <thead> <tr> <th>الإعداد</th> <th>السلوك</th> <th>الاستخدام</th> </tr> </thead> <tbody> <tr> <td><code>force-cache</code></td> <td>تخزين دائم</td> <td>بيانات ثابتة</td> </tr> <tr> <td><code>no-store</code></td> <td>لا تخزين</td> <td>بيانات حية</td> </tr> <tr> <td><code>revalidate: 60</code></td> <td>كل 60 ثانية</td> <td>بيانات دورية</td> </tr> </tbody> </table> ## Parallel Data Fetching **لجلب عدة مصادر بيانات بالتوازي:** ```tsx async function getData() { const [users, posts, comments] = await Promise.all([ fetch("https://api.example.com/users").then((r) => r.json()), fetch("https://api.example.com/posts").then((r) => r.json()), fetch("https://api.example.com/comments").then((r) => r.json()), ]); return { users, posts, comments }; } export default async function Page() { const { users, posts, comments } = await getData(); return ( <div> <p>المستخدمون: {users.length}</p> <p>المقالات: {posts.length}</p> <p>التعليقات: {comments.length}</p> </div> ); } ``` **الفائدة:** **أسرع 3 مرات** من التسلسل. ## Sequential Data Fetching **إذا كانت البيانات تعتمد على بعضها:** ```tsx async function getUser(id: string) { const res = await fetch(`https://api.example.com/users/${id}`); return res.json(); } async function getPosts(userId: string) { const res = await fetch(`https://api.example.com/users/${userId}/posts`); return res.json(); } export default async function UserPage() { const user = await getUser("1"); // ← أولاً const posts = await getPosts(user.id); // ← بعدها return ( <div> <h1>{user.name}</h1> <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> </div> ); } ``` ## Suspense و Streaming **Suspense** تسمح بتحميل الصفحة **بالتدريج**. ```tsx import { Suspense } from "react"; async function SlowComponent() { const data = await fetch("https://api.example.com/slow").then((r) => r.json()); return <p>{data.value}</p>; } async function FastComponent() { const data = await fetch("https://api.example.com/fast").then((r) => r.json()); return <p>{data.value}</p>; } export default function Dashboard() { return ( <div> <h1>لوحة التحكم</h1> <Suspense fallback={<p>جاري تحميل السريع...</p>}> <FastComponent /> </Suspense> <Suspense fallback={<p>جاري تحميل البطيء...</p>}> <SlowComponent /> </Suspense> </div> ); } ``` **النتيجة:** الجزء السريع يظهر أولاً، والبطيء لاحقاً. ## loading.tsx **لصفحة كاملة:** ```tsx // app/users/loading.tsx export default function Loading() { return ( <div className="flex items-center justify-center p-12"> <div className="animate-spin rounded-full h-12 w-12 border-4 border-blue-600 border-t-transparent" /> </div> ); } ``` **النتيجة:** تظهر شاشة تحميل أثناء جلب البيانات. ## error.tsx **لمعالجة الأخطاء:** ```tsx // app/users/error.tsx "use client"; export default function Error({ error, reset, }: { error: Error; reset: () => void; }) { return ( <div className="text-center p-12"> <h1 className="text-2xl font-bold text-red-600 mb-4"> ❌ حدث خطأ </h1> <p className="text-gray-600 mb-6">{error.message}</p> <button onClick={reset} className="bg-blue-600 text-white px-6 py-3 rounded-lg" > حاول مرة أخرى </button> </div> ); } ``` ## Server Actions (البيانات المتغيرة) **Server Actions** هي دوال تعمل على **السيرفر**، وتُستدعى من العميل. ### 1. تعريف Server Action **`app/actions.ts`:** ```tsx "use server"; export async function createPost(formData: FormData) { const title = formData.get("title") as string; const content = formData.get("content") as string; // ... حفظ في قاعدة البيانات console.log("عنوان:", title); console.log("محتوى:", content); // إعادة التحقق من المسار // revalidatePath("/posts"); } ``` ### 2. استخدامها في Client **`app/posts/new/page.tsx`:** ```tsx import { createPost } from "@/app/actions"; export default function NewPostPage() { return ( <form action={createPost} className="max-w-lg mx-auto p-8"> <h1 className="text-2xl font-bold mb-6">مقال جديد</h1> <input name="title" placeholder="العنوان" required className="w-full px-4 py-3 border rounded-lg mb-4" /> <textarea name="content" placeholder="المحتوى" required rows={6} className="w-full px-4 py-3 border rounded-lg mb-4" /> <button type="submit" className="bg-blue-600 text-white px-6 py-3 rounded-lg" > نشر </button> </form> ); } ``` **الفائدة:** لا تحتاج API Route! ## مثال عملي: مدونة مع Server Actions ### 1. Actions `app/posts/actions.ts` ```tsx "use server"; import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; let posts = [ { id: 1, title: "مقال 1", content: "..." }, ]; export async function createPost(formData: FormData) { const title = formData.get("title") as string; const content = formData.get("content") as string; const newPost = { id: Date.now(), title, content, }; posts.push(newPost); revalidatePath("/posts"); redirect("/posts"); } export async function deletePost(id: number) { posts = posts.filter((p) => p.id !== id); revalidatePath("/posts"); } export async function getPosts() { return posts; } ``` ### 2. الصفحة `app/posts/page.tsx` ```tsx import Link from "next/link"; import { getPosts } from "./actions"; export default async function PostsPage() { const posts = await getPosts(); return ( <div className="max-w-4xl mx-auto p-8"> <div className="flex justify-between items-center mb-8"> <h1 className="text-3xl font-bold">المقالات</h1> <Link href="/posts/new" className="bg-blue-600 text-white px-6 py-3 rounded-lg" > + مقال جديد </Link> </div> <ul className="space-y-4"> {posts.map((post) => ( <li key={post.id} className="bg-white p-6 rounded-xl shadow-sm"> <h2 className="text-xl font-bold mb-2">{post.title}</h2> <p className="text-gray-600">{post.content}</p> </li> ))} </ul> </div> ); } ``` ### 3. صفحة جديدة `app/posts/new/page.tsx` ```tsx import { createPost } from "../actions"; export default function NewPostPage() { return ( <form action={createPost} className="max-w-lg mx-auto p-8"> <h1 className="text-2xl font-bold mb-6">مقال جديد</h1> <input name="title" placeholder="العنوان" required className="w-full px-4 py-3 border rounded-lg mb-4" /> <textarea name="content" placeholder="المحتوى" required rows={6} className="w-full px-4 py-3 border rounded-lg mb-4" /> <button type="submit" className="bg-blue-600 text-white px-6 py-3 rounded-lg" > نشر </button> </form> ); } ``` ## useEffect في Client **للبحث المباشر، أو البيانات التي تتغير بناءً على تفاعل المستخدم:** ```tsx "use client"; import { useState, useEffect } from "react"; export default function SearchPage() { const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); useEffect(() => { if (!query) { setResults([]); return; } setLoading(true); const timer = setTimeout(async () => { const res = await fetch(`/api/search?q=${query}`); const data = await res.json(); setResults(data); setLoading(false); }, 500); return () => clearTimeout(timer); }, [query]); return ( <div className="max-w-2xl mx-auto p-8"> <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="ابحث..." className="w-full px-4 py-3 border rounded-lg mb-4" /> {loading && <p>جاري البحث...</p>} <ul> {results.map((r) => ( <li key={r.id}>{r.title}</li> ))} </ul> </div> ); } ``` ## جدول أوقات جلب البيانات <table> <thead> <tr> <th>الأسلوب</th> <th>المكان</th> <th>التوقيت</th> </tr> </thead> <tbody> <tr> <td><strong>Server + fetch</strong></td> <td>السيرفر</td> <td>قبل العرض</td> </tr> <tr> <td><strong>Server Actions</strong></td> <td>السيرفر</td> <td>عند إرسال النموذج</td> </tr> <tr> <td><strong>useEffect</strong></td> <td>المتصفح</td> <td>بعد العرض</td> </tr> <tr> <td><strong>SWR</strong></td> <td>المتصفح</td> <td>بعد العرض (مع cache)</td> </tr> </tbody> </table> ## تمارين عملية ### تمرين 1: جلب مستخدمين اجلب قائمة مستخدمين من API. **الحل:** ```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: cache: no-store اجلب بيانات لا تُخزّن. **الحل:** ```tsx const res = await fetch(url, { cache: "no-store" }); ``` ### تمرين 3: ISR اجلب بيانات مع إعادة تحقق كل 60 ثانية. **الحل:** ```tsx const res = await fetch(url, { next: { revalidate: 60 } }); ``` ### تمرين 4: Parallel Fetching اجلب مصدرين بالتوازي. **الحل:** ```tsx const [users, posts] = await Promise.all([ fetch("...").then((r) => r.json()), fetch("...").then((r) => r.json()), ]); ``` ### تمرين 5: Suspense أضف Suspense لمكون بطيء. **الحل:** ```tsx <Suspense fallback={<p>جاري التحميل...</p>}> <SlowComponent /> </Suspense> ``` ### تمرين 6: Server Action أنشئ Server Action لإضافة عنصر. **الحل:** ```tsx "use server"; export async function addItem(formData: FormData) { const name = formData.get("name") as string; // ... حفظ revalidatePath("/items"); } ``` ### تمرين 7: useEffect أنشئ بحثاً مع debouncing. **الحل:** (راجع مثال البحث أعلاه) ### تمرين 8: تطبيق كامل ابنِ تطبيق CRUD كامل مع Server Actions. **الحل:** (راجع مثال المدونة أعلاه) ## حل المشاكل الشائعة ### 🔴 المشكلة 1: البيانات لا تُحدّث **السبب:** cache يخزّن البيانات. **الحل:** ```tsx fetch(url, { cache: "no-store" }); ``` أو: ```tsx import { revalidatePath } from "next/cache"; revalidatePath("/posts"); ``` ### 🔴 المشكلة 2: `fetch` لا يعمل في Client **السبب:** تحتاج URL كامل. **الحل:** استخدم URL نسبي أو كامل: ```tsx fetch("/api/users"); // ← نسبي fetch("https://api.example.com/users"); // ← كامل ``` ### 🔴 المشكلة 3: Suspense لا يعمل **السبب:** المكون ليس `async`. **الحل:** تأكد من أن المكون `async`. ### 🔴 المشكلة 4: Server Action لا يعمل **السبب:** لم تُضف `"use server"`. **الحل:** أضف في أعلى الملف. ### 🔴 المشكلة 5: `revalidatePath is not a function` **الحل:** ```tsx import { revalidatePath } from "next/cache"; ``` ## جدول أدوات البيانات <table> <thead> <tr> <th>الأداة</th> <th>الاستخدام</th> </tr> </thead> <tbody> <tr> <td><code>fetch</code></td> <td>جلب البيانات</td> </tr> <tr> <td><code>cache: "no-store"</code></td> <td>لا تخزين</td> </tr> <tr> <td><code>revalidate: 60</code></td> <td>ISR</td> </tr> <tr> <td><code>Promise.all</code></td> <td>Parallel</td> </tr> <tr> <td><code>Suspense</code></td> <td>Streaming</td> </tr> <tr> <td><code>Server Actions</code></td> <td>البيانات المتغيرة</td> </tr> <tr> <td><code>revalidatePath</code></td> <td>إعادة التحقق</td> </tr> </tbody> </table> ## قائمة تحقق نهائية <table> <thead> <tr> <th>المهمة</th> <th>الحالة</th> </tr> </thead> <tbody> <tr> <td>fetch في Server Component</td> <td>⬜</td> </tr> <tr> <td>cache: no-store</td> <td>⬜</td> </tr> <tr> <td>ISR (revalidate)</td> <td>⬜</td> </tr> <tr> <td>Parallel Fetching</td> <td>⬜</td> </tr> <tr> <td>Suspense</td> <td>⬜</td> </tr> <tr> <td>Server Actions</td> <td>⬜</td> </tr> <tr> <td>حل التمارين الثمانية</td> <td>⬜</td> </tr> </tbody> </table> ## ماذا بعد هذا المقال؟ الآن بعد أن أتقنت جلب البيانات، أنت جاهز للمقال التالي: 1. **API Routes** — بناء APIs. 2. **Styling** — Tailwind و CSS. 3. **Authentication** — المصادقة. ## الخلاصة في هذا المقال، تعلمت: - ✅ fetch في Server Component. - ✅ cache (force, no-store, revalidate). - ✅ Parallel vs Sequential. - ✅ Suspense و Streaming. - ✅ loading.tsx و error.tsx. - ✅ Server Actions. - ✅ useEffect في Client. - ✅ حل 8 تمارين عملية. **تذكر:** جلب البيانات هو **قلب** أي تطبيق. أتقنه جيداً.