Next.js📅 2026-11-15⏱ 9 دقائق قراءة⚡ مقال 5 من 10

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

دليل عملي مفصل لجلب البيانات في Next.js — fetch، cache، ISR، Server Actions، مع تمارين وحلول

في المقال السابق، تعلمت Server Components. الآن سنتعمق في جلب البيانات (Data Fetching) — وهو جوهر أي تطبيق ويب حقيقي.

في هذا الدليل العملي، سنأخذك خطوة بخطوة لفهم جلب البيانات في Next.js، مع تمارين وحلول.

طرق جلب البيانات

الطريقةالمكانالاستخدام
fetch في Server Componentالسيرفرالأكثر شيوعاً
Server Actionsالسيرفرالبيانات المتغيرة (POST/PUT)
Route HandlersالسيرفرAPI Endpoints
useEffectالمتصفحالبيانات الديناميكية
SWR / React Queryالمتصفحالتخزين المؤقت المتقدم

fetch في Server Component

أسهل طريقة: استخدام fetch مباشرة في Server Component.

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)

fetch(url, { cache: "force-cache" });  // ← افتراضي

السلوك: تُجلب البيانات مرة واحدة، وتُخزّن للأبد.

الاستخدام: بيانات لا تتغير (مقالات، صفحات ثابتة).

2. لا تخزين (Dynamic)

fetch(url, { cache: "no-store" });

السلوك: تُجلب البيانات في كل طلب.

الاستخدام: بيانات حية (أسعار، أخبار).

3. إعادة التحقق (ISR)

fetch(url, { next: { revalidate: 60 } });  // ← كل 60 ثانية

السلوك: تُجلب البيانات، وتُخزّن، وتُحدَّث كل 60 ثانية.

الاستخدام: بيانات تتغير بشكل دوري.

الإعدادالسلوكالاستخدام
force-cacheتخزين دائمبيانات ثابتة
no-storeلا تخزينبيانات حية
revalidate: 60كل 60 ثانيةبيانات دورية

Parallel Data Fetching

لجلب عدة مصادر بيانات بالتوازي:

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

إذا كانت البيانات تعتمد على بعضها:

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 تسمح بتحميل الصفحة بالتدريج.

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

لصفحة كاملة:

// 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

لمعالجة الأخطاء:

// 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:

"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:

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

"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

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

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

للبحث المباشر، أو البيانات التي تتغير بناءً على تفاعل المستخدم:

"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>
  );
}

جدول أوقات جلب البيانات

الأسلوبالمكانالتوقيت
Server + fetchالسيرفرقبل العرض
Server Actionsالسيرفرعند إرسال النموذج
useEffectالمتصفحبعد العرض
SWRالمتصفحبعد العرض (مع cache)

تمارين عملية

تمرين 1: جلب مستخدمين

اجلب قائمة مستخدمين من API.

الحل:

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

اجلب بيانات لا تُخزّن.

الحل:

const res = await fetch(url, { cache: "no-store" });

تمرين 3: ISR

اجلب بيانات مع إعادة تحقق كل 60 ثانية.

الحل:

const res = await fetch(url, { next: { revalidate: 60 } });

تمرين 4: Parallel Fetching

اجلب مصدرين بالتوازي.

الحل:

const [users, posts] = await Promise.all([
  fetch("...").then((r) => r.json()),
  fetch("...").then((r) => r.json()),
]);

تمرين 5: Suspense

أضف Suspense لمكون بطيء.

الحل:

<Suspense fallback={<p>جاري التحميل...</p>}>
  <SlowComponent />
</Suspense>

تمرين 6: Server Action

أنشئ Server Action لإضافة عنصر.

الحل:

"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 يخزّن البيانات.

الحل:

fetch(url, { cache: "no-store" });

أو:

import { revalidatePath } from "next/cache";
revalidatePath("/posts");

🔴 المشكلة 2: fetch لا يعمل في Client

السبب: تحتاج URL كامل.

الحل: استخدم URL نسبي أو كامل:

fetch("/api/users");  // ← نسبي
fetch("https://api.example.com/users");  // ← كامل

🔴 المشكلة 3: Suspense لا يعمل

السبب: المكون ليس async.

الحل: تأكد من أن المكون async.

🔴 المشكلة 4: Server Action لا يعمل

السبب: لم تُضف "use server".

الحل: أضف في أعلى الملف.

🔴 المشكلة 5: revalidatePath is not a function

الحل:

import { revalidatePath } from "next/cache";

جدول أدوات البيانات

الأداةالاستخدام
fetchجلب البيانات
cache: "no-store"لا تخزين
revalidate: 60ISR
Promise.allParallel
SuspenseStreaming
Server Actionsالبيانات المتغيرة
revalidatePathإعادة التحقق

قائمة تحقق نهائية

المهمةالحالة
fetch في Server Component⬜
cache: no-store⬜
ISR (revalidate)⬜
Parallel Fetching⬜
Suspense⬜
Server Actions⬜
حل التمارين الثمانية⬜

ماذا بعد هذا المقال؟

الآن بعد أن أتقنت جلب البيانات، أنت جاهز للمقال التالي:

  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 تمارين عملية.

تذكر: جلب البيانات هو قلب أي تطبيق. أتقنه جيداً.

📚 مقالات ذات صلة

Next.js

مشروع Next.js متكامل — مدونة احترافية 2026

مشروع عملي شامل لبناء مدونة كاملة بـ Next.js — مع Markdown، SEO، نشر تلقائي، وتصميم احترافي

Next.js

نشر تطبيق Next.js — دليل شامل 2026

دليل عملي مفصل لنشر تطبيق Next.js — Vercel، Firebase، Netlify، والتصدير الثابت

Next.js

المصادقة في Next.js — دليل شامل 2026

دليل عملي مفصل للمصادقة في Next.js — NextAuth.js، JWT، Middleware، وحماية المسارات