Next.js: جلب البيانات: fetch و RSC

آخر تحديث: 2026-08-26

في RSC، لم يعد fetch مجرد جلب من المتصفح — بل يمتد ليشمل طبقة التخزين المؤقت، مما يتيح لك التحكم في دورة حياة البيانات بطريقة تعريفية.

1. ما ستتعلمه



2. قصة حقيقية لمطور Full-Stack

(1) نقطة الألم: لوحة التحكم تستغرق 8 ثوانٍ للتحميل

Bob هو القائد التقني لفريق TaskFlow. تحتاج صفحة Dashboard إلى تحميل خمسة مصادر بيانات: إحصائيات المستخدمين، إجمالي عدد المشاريع، المهام الأخيرة، سجلات النشاط، وإشعارات النظام. استخدم الكود الأولي خمس استدعاءات await fetch(...) تسلسلية، كل منها ينتظر اكتمال السابق — مما أدى إلى وقت إجمالي قدره 2.1s + 1.8s + 1.5s + 0.9s + 1.7s = 8 ثوانٍ. اشتكى المستخدمون من أن الصفحة "تستغرق وقتًا طويلاً جدًا للتحميل". والأسوأ من ذلك، كان يتم إعادة الاستعلام عن API مع كل تحديث، مما تسبب في ارتفاع حمل قاعدة البيانات إلى 5,000 QPS.

(2) حل Next.js fetch

استخدم Promise.all() للطلبات المتوازية + next: { revalidate: 60 } لتخزين مؤقت لمدة 60 ثانية.

TSX
// app/dashboard/page.tsx
export default async function DashboardPage() {
  const [users, projects, tasks, logs, notifs] = await Promise.all([
    fetch('https://api.example.com/stats/users', { next: { revalidate: 60 } }),
    fetch('https://api.example.com/stats/projects', { next: { revalidate: 60 } }),
    fetch('https://api.example.com/stats/tasks', { next: { revalidate: 30 } }),
    fetch('https://api.example.com/activity/logs', { cache: 'no-store' }),
    fetch('https://api.example.com/notifications', { next: { revalidate: 10 } }),
  ]).then(responses => Promise.all(responses.map(r => r.json())))

  return <DashboardView {...{ users, projects, tasks, logs, notifs }} />
}

(3) النتائج

البُعد قبل التحسين بعد التحسين
وقت تحميل الصفحة 8 ثوانٍ (تسلسلي) 2.1 ثانية (متوازي)
QPS قاعدة البيانات 5,000 83 (تخزين مؤقت 60 ثانية)
شكاوى المستخدمين 12 يوميًا 0
عدد أسطر الكود 35 سطرًا (5 استدعاءات منفصلة) 10 أسطر


3. أنماط التخزين المؤقت الثلاثة لـ fetch

يوسع Next.js 16 واجهة fetch الخاصة بالويب بإضافة ثلاثة أنماط للتخزين المؤقت. جميع استدعاءات fetch في RSC تستخدم force-cache (تخزين مؤقت تلقائي) افتراضيًا، ما لم يُحدد نمط آخر بشكل صريح.

100%
graph LR
    A[RSC fetch] --> B{نمط التخزين المؤقت}
    B --> C[force-cache<br/>القيمة الافتراضية]
    B --> D[no-store<br/>جلب جديد مع كل طلب]
    B --> E[revalidate:N<br/>نافذة زمنية]
    C --> F[ذاكرة البيانات المؤقتة<br/>تخزين دائم]
    D --> G[بيانات آنية<br/>بدون تخزين مؤقت]
    E --> H[مخزنة مؤقتًا لمدة N ثانية<br/>إعادة الجلب بعد الانتهاء]
    
    style C fill:#d4edda
    style D fill:#f8d7da
    style E fill:#fff3cd
النمط الصيغة السلوك حالات الاستخدام
force-cache (افتراضي) fetch(url) أو fetch(url, { cache: 'force-cache' }) يُسترد فقط أثناء البناء أو في أول طلب؛ النتائج تُخزن مؤقتًا بشكل دائم بيانات نادرًا ما تتغير (مستندات، إعدادات ثابتة)
no-store fetch(url, { cache: 'no-store' }) استرداد البيانات من جديد مع كل طلب؛ بدون تخزين مؤقت بيانات آنية (معلومات المستخدم، المخزون)
revalidate:N fetch(url, { next: { revalidate: 60 } }) مخزنة مؤقتًا لمدة 60 ثانية؛ عملية خلفية تشغّل تحديثًا عند الانتهاء بيانات شبه آنية (أخبار، لوحات المتصدرين)

(1) سلوك force-cache الافتراضي

إذا لم تُمرر أي خيارات، يقوم Next.js تلقائيًا بتخزين نتائج fetch مؤقتًا — الطلبات التي لها نفس URL والخيارات تُنفذ مرة واحدة فقط أثناء عملية البناء.

TSX
// app/products/page.tsx — force-cache افتراضي
export default async function ProductsPage() {
  const products = await fetch('https://api.example.com/products').then(r => r.json())
  // يُسترد مرة واحدة أثناء البناء، ويُستخدم التخزين المؤقت بعد ذلك
  return <ProductList data={products} />
}

(2) no-store بيانات ديناميكية

TSX
// app/profile/page.tsx — استرداد أحدث البيانات مع كل طلب
export default async function ProfilePage() {
  const user = await fetch('https://api.example.com/me', {
    cache: 'no-store',
    headers: { Authorization: `Bearer ${process.env.API_TOKEN}` }
  }).then(r => r.json())
  return <ProfileView user={user} />
}

(3) revalidate نافذة زمنية

TSX
// app/blog/[slug]/page.tsx — تخزين مؤقت بنمط ISR
export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await fetch(`https://cms.example.com/posts/${params.slug}`, {
    next: { revalidate: 3600 }  // استخدام التخزين المؤقت لمدة ساعة واحدة
  }).then(r => r.json())
  return <article><h1>{post.title}</h1><div>{post.content}</div></article>
}

▶ مثال: مقارنة أنماط التخزين المؤقت الثلاثة (مستوى الصعوبة: ⭐)

المخرجات:

TEXT 📖 للعرض فقط
يجلب البيانات ويُصيّر النتيجة.
TSX
// app/cache-demo/page.tsx
export default async function CacheDemoPage() {
  const staticData = await fetch('http://worldtimeapi.org/api/timezone/Etc/UTC', {
    cache: 'force-cache'
  }).then(r => r.json())

  const liveData = await fetch('http://worldtimeapi.org/api/timezone/Etc/UTC', {
    cache: 'no-store'
  }).then(r => r.json())

  return (
    <div>
      <p>ثابت (force-cache): {staticData.datetime}</p>
      <p>مباشر (no-store): {liveData.datetime}</p>
    </div>
  )
}

المخرجات:

TEXT 📖 للعرض فقط
ثابت (force-cache): 2026-07-06T10:00:00.000Z  ← دائمًا نفس القيمة
مباشر (no-store): 2026-07-06T10:00:05.123Z       ← يتغير مع كل تحديث

المخرجات:

TEXT 📖 للعرض فقط
المتصفح يُصيّر طابعين زمنيين:
  ثابت (force-cache): 2026-07-06T10:00:00.000Z  ← دائمًا نفس القيمة (مخزنة مؤقتًا عند البناء)
  مباشر (no-store): 2026-07-06T10:00:05.123Z       ← يتغير مع كل تحديث


4. إعادة التحقق عند الطلب: tags و revalidateTag

next: { tags: [...] } يوسم طلب fetch، ثم استخدم revalidateTag(tag) لتحديث التخزين المؤقت حسب الحاجة في Server Action أو Route Handler.

100%
sequenceDiagram
    participant A as Server Action
    participant Cache as ذاكرة البيانات المؤقتة
    participant DB as قاعدة البيانات

    A->>DB: كتابة بيانات جديدة (إنشاء مهمة)
    A->>Cache: revalidateTag('tasks')
    Cache->>Cache: مسح جميع التخزين المؤقت المطابق للوسم
    Note over Cache: في المرة القادمة fetch يُسترجع من جديد
API الغرض مكان الاستدعاء
next: { tags: ['tasks', 'projects'] } توسيم "fetch" خيارات fetch()
revalidateTag('tasks') مسح جميع التخزين المؤقت المرتبط بالوسم Server Action / Route Handler
revalidatePath('/dashboard') مسح التخزين المؤقت حسب المسار Server Action / Route Handler

▶ مثال: استخدام tags و revalidateTag (مستوى الصعوبة: ⭐⭐)

TSX
// app/tasks/data.ts — دوال استرجاع البيانات
export async function getTasks() {
  return fetch('https://api.example.com/tasks', {
    next: { tags: ['tasks'] }
  }).then(r => r.json())
}
TSX
// app/tasks/actions.ts — Server Action: تحديث التخزين المؤقت بعد الكتابة
'use server'
import { revalidateTag } from 'next/cache'

export async function createTask(formData: FormData) {
  const title = formData.get('title') as string
  await fetch('https://api.example.com/tasks', {
    method: 'POST',
    body: JSON.stringify({ title, status: 'todo' })
  })
  revalidateTag('tasks')  // مسح جميع مدخلات التخزين المؤقت لهذا الوسم
}

المخرجات:

TEXT 📖 للعرض فقط
يجلب البيانات ويُصيّر النتيجة.

▶ مثال: استخدام revalidatePath لمسح صفحة كاملة (مستوى الصعوبة: ⭐⭐)

TSX
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'

export async function publishArticle() {
  await db.article.update({ where: { id: 1 }, data: { published: true } })
  revalidatePath('/blog')       // تحديث صفحة /blog
  revalidatePath('/blog/[slug]') // تحديث جميع تفاصيل المقالات
}

المخرجات:

TEXT 📖 للعرض فقط
يُصيّر واجهة مكون publishArticle.


5. جلب البيانات المتوازي وتجنب تأثير الشلال (Waterfall)

نمط الشلال هو القاتل الأول للأداء — كل await ينتظر بالتسلسل اكتمال السابق. استخدام Promise.all() يتيح بدء جميع الطلبات في وقت واحد.

100%
graph LR
    subgraph "نمط الشلال (بطيء)"
        A1[fetch A] --> A2[fetch B] --> A3[fetch C]
        A1 -.- t1[2s]
        A2 -.- t2[+2s = 4s]
        A3 -.- t3[+2s = 6s]
    end
    subgraph "متوازي (سريع)"
        B1[fetch A] -.- u1[2s]
        C1[fetch B] -.- u2[2s]
        D1[fetch C] -.- u3[2s]
        B1 & C1 & D1 --> M[Promise.all<br/>الوقت الإجمالي ~2s]
    end
النمط التنفيذ الوقت الإجمالي (ثانيتان لكل منها) السيناريوهات المناسبة
شلال تسلسلي await A; await B; await C ~6s طلبات مترابطة
طلبات متوازية Promise.all([A, B, C]) ~2s طلبات مستقلة غير مرتبطة
معالجة متوازية مرحلية const a = await A; const [b, c] = await Promise.all([B(a.id), C]) ~4s طلبات مترابطة جزئيًا

▶ مثال: التعرف على نمط الشلال التسلسلي (مستوى الصعوبة: ⭐)

TSX
// app/waterfall/page.tsx — ❌ شلال تسلسلي
export default async function WaterfallPage() {
  const user = await fetch('https://api.example.com/user').then(r => r.json())          // 1s
  const tasks = await fetch(`https://api.example.com/tasks?userId=${user.id}`).then(r => r.json())  // انتظار حتى الانتهاء + 2s = 3s
  const details = await Promise.all(tasks.map(t =>
    fetch(`https://api.example.com/tasks/${t.id}/details`).then(r => r.json())  // انتظار حتى الانتهاء + 2s = 5s
  ))

  return <div>الإجمالي: ~5s</div>
}

المخرجات:

TEXT 📖 للعرض فقط
يجلب البيانات ويُصيّر قائمة من العناصر.
النص المرئي: الإجمالي: ~5s

▶ مثال: التحسين المتوازي (مستوى الصعوبة: ⭐⭐)

المخرجات:

TEXT 📖 للعرض فقط
تُصيّر الصفحة كما هو موصوف أعلاه، مع تحديث واجهة المستخدم بناءً على السلوك الموصوف.
TSX
// app/no-waterfall/page.tsx — ✅ تحسين متوازي
export default async function NoWaterfallPage() {
  // المرحلة 1: استرجاع المستخدمين والبيانات الأولية بشكل متزامن
  const [user, initialData] = await Promise.all([
    fetch('https://api.example.com/user', { next: { revalidate: 10 } }).then(r => r.json()),
    fetch('https://api.example.com/initial', { cache: 'no-store' }).then(r => r.json()),
  ])

  // المرحلة 2: طلب يعتمد على user.id (لا يزال هناك شلال صغير، لكنه أفضل ما يمكن)
  const tasks = await fetch(`https://api.example.com/tasks?userId=${user.id}`).then(r => r.json())

  return <div>الإجمالي: ~2s (1s + 1s متوازي، ثم 1s)</div>
}

المخرجات:

TEXT 📖 للعرض فقط
يجلب البيانات ويُصيّر النتيجة.
النص المرئي: الإجمالي: ~2s (1s + 1s متوازي، ثم 1s)

▶ مثال: التحميل الكسول مع Suspense (مستوى الصعوبة: ⭐⭐⭐)

المخرجات:

TEXT 📖 للعرض فقط
تُصيّر الصفحة كما هو موصوف أعلاه، مع تحديث واجهة المستخدم بناءً على السلوك الموصوف.
TSX
// app/suspense-demo/page.tsx — كل منطقة منفصلة تستخدم تغليف Suspense
import { Suspense } from 'react'

export default function SuspenseDemoPage() {
  return (
    <div>
      <h1>لوحة التحكم</h1>
      <Suspense fallback={<div>جاري تحميل الملف الشخصي...</div>}>
        <ProfileSection />
      </Suspense>
      <Suspense fallback={<div>جاري تحميل المهام...</div>}>
        <TaskSection />
      </Suspense>
    </div>
  )
}

async function ProfileSection() {
  const user = await fetch('https://api.example.com/user', { cache: 'no-store' }).then(r => r.json())
  return <div>مرحبًا، {user.name}</div>
}

async function TaskSection() {
  const tasks = await fetch('https://api.example.com/tasks', { next: { revalidate: 30 } }).then(r => r.json())
  return <ul>{tasks.map((t: any) => <li key={t.id}>{t.title}</li>)}</ul>
}

المخرجات:

TEXT 📖 للعرض فقط
يُصيّر هيكلًا ثابتًا فورًا، مع تحميل المحتوى الديناميكي داخل حدود Suspense.
المحتوى الاحتياطي: جاري تحميل الملف الشخصي...
النص المرئي: لوحة التحكم | جاري تحميل الملف الشخصي... | }> | جاري تحميل المهام...


6. مثال كامل: لوحة تحكم محسّنة

TSX
// app/dashboard-optimized/page.tsx
import { Suspense } from 'react'
import { revalidateTag } from 'next/cache'

// ======== دوال البيانات ========
const API = 'https://jsonplaceholder.typicode.com'

async function getData<T>(endpoint: string, options?: RequestInit): Promise<T> {
  const res = await fetch(`${API}${endpoint}`, {
    ...options,
    next: { tags: [endpoint.split('/')[1] ?? 'default'], ...(options as any)?.next },
  })
  if (!res.ok) throw new Error(`فشل في جلب ${endpoint}`)
  return res.json()
}

// ======== استرجاع جميع البيانات بشكل متوازي ========
export default function DashboardOptimizedPage() {
  return (
    <div>
      <h1>لوحة تحكم محسّنة</h1>
      <div style={{ display: 'grid', gap: 16, gridTemplateColumns: '1fr 1fr' }}>
        <Suspense fallback={<Skeleton label="المستخدمين" />}>
          <DataCard title="المستخدمين" endpoint="/users" />
        </Suspense>
        <Suspense fallback={<Skeleton label="المنشورات" />}>
          <DataCard title="المنشورات" endpoint="/posts" revalidate={120} />
        </Suspense>
        <Suspense fallback={<Skeleton label="التعليقات" />}>
          <DataCard title="التعليقات" endpoint="/comments" />
        </Suspense>
        <Suspense fallback={<Skeleton label="المهام" />}>
          <DataCard title="المهام" endpoint="/todos" revalidate={30} />
        </Suspense>
      </div>
    </div>
  )
}

async function DataCard({ title, endpoint, revalidate }: {
  title: string
  endpoint: string
  revalidate?: number
}) {
  const data = await getData<any[]>(endpoint, revalidate
    ? { next: { revalidate } }
    : { cache: 'no-store' }
  )
  return (
    <div style={{ border: '1px solid #ddd', borderRadius: 8, padding: 16 }}>
      <h2>{title} <span style={{ fontSize: 14, color: '#666' }}>({data.length})</span></h2>
      <ul>{data.slice(0, 5).map((item: any) => (
        <li key={item.id}>{item.title ?? item.name ?? item.email}</li>
      ))}</ul>
    </div>
  )
}

function Skeleton({ label }: { label: string }) {
  return <div style={{ border: '1px solid #eee', borderRadius: 8, padding: 16, opacity: 0.5 }}>
    جاري تحميل {label}...
  </div>
}

// app/dashboard-optimized/actions.ts
'use server'
import { revalidateTag } from 'next/cache'

export async function refreshSection(tag: string) {
  revalidateTag(tag)
  return { success: true }
}


❓ أسئلة شائعة

س هل خيارا force-cache و no-store في fetch يتصرفان بنفس الطريقة في وضع التطوير ووضع الإنتاج؟
ج لا، لا يتصرفان بنفس الطريقة. في وضع التطوير (npm run dev)، لا يزال force-cache يُجلب مع كل طلب (لتسهيل التصحيح). التخزين المؤقت لا يسري إلا في وضع الإنتاج (next start أو بعد البناء). هذا قرار تصميمي في Next.js — لجلب أحدث البيانات دائمًا أثناء مرحلة التطوير.
س ما الفرق بين revalidateTag و revalidatePath؟
ج revalidateTag يمسح التخزين المؤقت حسب الوسم (لنفس البيانات عبر صفحات مختلفة)، بينما revalidatePath يمسح التخزين المؤقت حسب المسار (وصولاً إلى الصفحة أو نمط المسار). الأول مناسب للتحكم الدقيق في طبقة البيانات، بينما الثاني مناسب للتحديث على مستوى الصفحة. نوصي باستخدام revalidateTag كلما أمكن.
س إذا استخدم طلبا fetch نفس URL لكن بخيارات مختلفة، هل سيتشاركان التخزين المؤقت؟
ج لا. مفتاح التخزين المؤقت يُحسب بناءً على URL والطريقة (method) والترويسات (headers) والجسم (body). الطلبات التي لها نفس URL لكن إعدادات cache أو next.revalidate مختلفة ستُعامل كمدخلات تخزين مؤقت مختلفة.
س كيف يتعامل Promise.all مع طلب فاشل؟
ج Promise.all هي عملية "الكل أو لا شيء" — إذا فشل أي طلب فردي، يتم رفض الـ promise بالكامل. إذا كنت بحاجة إلى معالجة الأخطاء، قم بتغليف كل استدعاء fetch في Promise.allSettled أو كتلة try-catch. النمط الشائع هو const results = await Promise.all(urls.map(u => fetch(u).catch(() => null))).
س كيف يتم التعامل مع مهلة fetch في RSC؟
ج fetch لا يحتوي على مهلة مدمجة. يمكنك تغليفه في AbortController: const ctrl = new AbortController(); setTimeout(() => ctrl.abort(), 5000); fetch(url, { signal: ctrl.signal }). يُنصح بتغليف عميل fetch موحد على مستوى التطبيق.
س هل يمكنني استخدام عميل HTTP تابع لجهة خارجية (مثل axios) في RSC؟
ج نعم، لكنك ستفقد الميزات الموسعة لـ fetch في Next.js، مثل التخزين المؤقت التلقائي والوسوم (tags) وإعادة التحقق (revalidation). إذا استخدمت axios، ستحتاج إلى تنفيذ منطق التخزين المؤقت يدويًا أو تغليف طبقة متوافقة مع fetch حول axios. نوصي باستخدام طريقة fetch الأصلية كلما أمكن.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (⭐): أنشئ app/time-demo/page.tsx، واستخدم cache: 'no-store' و cache: 'force-cache' لإجراء طلبات منفصلة إلى World Time API، وقارن الفرق بين الطابعين الزمنيين، وتحقق من سلوك التخزين المؤقت.

  2. تمرين متقدم (⭐⭐): ابنِ app/parallel-demo/page.tsx يستخدم Promise.all لجلب /users و/posts و/comments (باستخدام JSONPlaceholder API)، وصيّر كل نقطة بيانات داخل مربع حدود <Suspense> منفصل لعرض تأثير التحميل بالتدفق.

  3. تحدٍّ (⭐⭐⭐): أنشئ صفحة قائمة مهام تدعم عمليات CRUD: app/tasks/page.tsx (عرض قائمة المهام، باستخدام وسوم للتخزين المؤقت) وapp/tasks/actions.ts (استدعاء revalidateTag('tasks') لتحديث القائمة بعد إضافة أو حذف مهمة). نفذ تحديثًا تفاؤليًا (optimistic update) لضمان تحديث القائمة فورًا بعد عملية الكتابة.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%