تطوير الويب

Data fetching وcaching في Next.js 15: درس خطوة بخطوة

3 min de lecture

السلسلة: هذا الدرس جزء من سلسلة 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 للاختبار

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

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité