السلسلة: هذا الدرس جزء من سلسلة React 19 وNext.js 15. للحصول على نظرة شاملة، اقرأ المقال الرئيسي.
مع App Router وServer Components، data fetching في Next.js تغيّر روحه كلياً. لا نفكّر بـ useEffect + useState + loading، ولا بـ getServerSideProps. نكتب كوداً async مباشرة في المكوّنات الخادمية، وNext.js يُنسّق العرض.
التغيير الأكبر مقارنة بـ Next.js 14 يخصّ caching. في 14، fetch() كان مُخزَّناً عدوانياً افتراضياً. في 15، الفريق عكس الافتراض: كل شيء ديناميكي إلا بإشارة معاكسة.
المتطلبات
- Next.js 15.x مع App Router
- فهم Server vs Client Components
- 90 دقيقة
الخطوة 1 — fetch بسيط في Server Component
// src/app/articles/page.tsx
type Article = { id: number; title: string; body: string };
export default async function ListeArticles() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const articles: Article[] = await res.json();
return (
<section>
<h1 className="text-2xl font-bold mb-4">Articles</h1>
<ul className="space-y-3">
{articles.slice(0, 10).map((a) => (
<li key={a.id} className="border-b pb-2">
<h2 className="font-semibold">{a.title}</h2>
<p className="text-sm text-gray-600 line-clamp-2">{a.body}</p>
</li>
))}
</ul>
</section>
);
}
زر /articles. الصفحة تُعرَض. إن أعدت التحميل عدة مرات، الـ fetch يُنفَّذ كل طلب — السلوك الافتراضي في Next.js 15. الصفحة ديناميكية إذن.
الخطوة 2 — التخزين الصريح مع revalidate
const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
next: { revalidate: 60 }, // re-fetch كل 60 ثانية كحد أقصى
});
بهذا، Next.js يخزّن الردّ 60 ثانية. الزائر الأول يُطلق fetch؛ الباقون يُخدَمون من cache. بعد 60 ثانية، الطلب التالي يُخدَم من cache stale لكن يُطلق re-fetch خلفياً (stale-while-revalidate).
للتخزين لا نهائياً حتى إلغاء يدوي:
const res = await fetch('https://api.exemple.com/produits', {
next: { revalidate: false, tags: ['produits'] },
});
// لاحقاً في Server Action
import { revalidateTag } from 'next/cache';
revalidateTag('produits');
أدقّ من revalidatePath، الذي يُلغي كل fetches في route معيّن بلا تمييز.
الخطوة 3 — العرض الثابت مقابل الديناميكي
صفحة تُعتبَر ثابتة إن أمكن حلّ كل مصادر بياناتها في وقت build. ديناميكية إن اعتمدت مصدر على الطلب (cookies، headers، searchParams، fetch بلا cache).
// src/app/dashboard/page.tsx
export const dynamic = 'force-dynamic';
export default async function Dashboard() {
// دائماً تُعرَض عند الطلب
}
المعاكس: export const dynamic = 'force-static' يفرض build ثابت ويكسر صراحة إن استخدم الكود API ديناميكياً. لتحقّق ما يعتبره Next.js ثابتاً أو ديناميكياً، شغّل pnpm build: ● = ثابتة مُعرَّضة مسبقاً، ○ = ثابتة عند الطلب، ƒ = ديناميكية.
الخطوة 4 — fetches متوازية بـ Promise.all
// بطيء — في تسلسل (200ms + 300ms = 500ms)
const utilisateur = await getUtilisateur(id);
const commandes = await getCommandes(id);
// سريع — متوازٍ (max(200, 300) = 300ms)
const utilisateurPromise = getUtilisateur(id);
const commandesPromise = getCommandes(id);
const [utilisateur, commandes] = await Promise.all([
utilisateurPromise,
commandesPromise,
]);
الخطوة 5 — نمط Suspense + Streaming
بدل انتظار جهوزية كل الصفحة، نُرسل HTML للمتصفّح بمجرّد عرض الأقسام السريعة، ونُبثّ الأقسام البطيئة تباعاً.
// src/app/produits/[slug]/page.tsx
import { Suspense } from 'react';
import { FicheProduit } from '@/components/FicheProduit';
import { ListeAvis } from '@/components/ListeAvis';
export default function PageProduit({ params }: { params: Promise<{ slug: string }> }) {
return (
<div>
<FicheProduit params={params} />
<Suspense fallback={<p>Chargement des avis...</p>}>
<ListeAvis params={params} />
</Suspense>
</div>
);
}
إن استغرق ListeAvis ثانيتين، المستخدم يرى بطاقة المنتج فوراً، ثم تظهر الـ avis. Next.js 15 يُمرّر آلياً AbortSignal في Server Components بفضل hook الجديد cacheSignal من React 19.2.
الخطوة 6 — الإلغاء التكراري بـ React cache()
// src/lib/db.ts
import { cache } from 'react';
import { prisma } from './prisma';
export const getUtilisateur = cache(async (id: string) => {
return prisma.user.findUnique({ where: { id } });
});
الآن، مهما كان عدد المكوّنات التي تستدعي getUtilisateur('abc') خلال عرض طلب واحد، القاعدة تُستجوَب مرة واحدة فقط.
الخطوة 7 — التوجيه ‘use cache’ (تجريبي)
Next.js 15 يُدخل توجيهاً جديداً: 'use cache'. يُفعَّل في next.config.js عبر experimental.useCache: true.
// src/lib/produits.ts
'use cache';
export async function getProduitsPopulaires() {
const res = await prisma.produit.findMany({
orderBy: { ventes: 'desc' },
take: 10,
});
return res;
}
لتحكّم مدة الحياة، أضف cacheLife('hours') مع profiles مُعرَّفة مسبقاً (seconds، minutes، hours، days، weeks، max). للإلغاء، cacheTag('produits-populaires') ثم revalidateTag. لا يزال تجريبياً — للإنتاج، ابقَ على fetch + next: { revalidate, tags }.
الخطوة 8 — الإلغاء من Server Action
// src/app/admin/produits/page.tsx
import { revalidateTag } from 'next/cache';
async function ajouterProduit(formData: FormData) {
'use server';
const nom = formData.get('nom') as string;
await prisma.produit.create({ data: { nom } });
revalidateTag('produits');
}
export default async function Admin() {
const produits = await fetch('https://api.exemple.com/produits', {
next: { tags: ['produits'] },
}).then((r) => r.json());
return (
<>
<form action={ajouterProduit}>
<input name="nom" />
<button>Ajouter</button>
</form>
<ul>{produits.map((p: any) => <li key={p.id}>{p.nom}</li>)}</ul>
</>
);
}
الخطوة 9 — اختيار مدة cache المناسبة
| نوع البيانات | المدة الموصى بها | الاستراتيجية |
|---|---|---|
| كتالوغ منتجات (تغييرات يومية) | 3600 (ساعة) مع tag | Tag produits يُلغى على إنشاء/تعديل |
| صفحة استقبال تحريرية | 86400 (24 ساعة) مع tag | إلغاء يدوي عبر Server Action |
| ملف مستخدم متصل | بلا cache (dynamic) | كل طلب يُحمّل من القاعدة |
| بيانات analytics مُجمَّعة | 300 (5 دقائق) | تأخّر مقبول، ربح CPU كبير |
| قائمة فئات (تتغير نادراً) | revalidate: false + tag |
cache لا نهائي، إلغاء يدوي |
الخطوة 10 — تشخيص cache عملياً
curl -I https://votre-app.com/produits | grep -i cache
# x-vercel-cache: HIT → خُدِم من cache CDN
# x-nextjs-cache: STALE → stale-while-revalidate قيد التحديث
# x-vercel-cache: MISS → أول طلب أو cache منتهٍ
أخطاء شائعة
| الخطأ | السبب | الحل |
|---|---|---|
| بيانات تبقى ثابتة لا نهائياً | كود مكتوب لـ Next.js 14 أو force-static |
تحقّق من 15.x؛ راجع fetches |
| صفحة ديناميكية رغم رغبة الثبات | استدعاء cookies()، headers()، أو fetch بلا cache |
حدّد المصدر عبر pnpm build؛ خزّن صراحة |
| fetches في تسلسل بلا سبب | await متعاقبة بدل Promise.all |
أعد البنية لإطلاق الوعود متوازية |
cache لا يُلغى رغم revalidateTag |
الـ tag غير مرتبط بـ fetch الأصلي | تحقّق من next: { tags: ['x'] }؛ الـ tag حسّاس للحالة |
| «Route is configured with both dynamic and revalidate» | تعارض force-static مع fetch ديناميكي |
اختر أحدهما |
| Suspense fallback لا يظهر أبداً | المكوّن ليس async فعلاً أو البيانات في cache | أكّد بـ setTimeout في دالة fetch للاختبار |