جلب البيانات من APIs في Next.js

جلب البيانات من APIs في Next.js

يُعدّ جلب البيانات من APIs في Next.js أحد أكثر المهام اليومية التي يواجهها أي مطوّر واجهات أمامية، فمعظم التطبيقات الحديثة لم تعد تعرض بيانات ثابتة، بل تستهلك واجهات برمجية (APIs) لجلب المقالات والمنتجات وبيانات المستخدمين والإحصائيات. ما يميّز Next.js هو أنه يمنحك نموذجاً هجيناً يجمع بين التنفيذ على الخادم والتنفيذ داخل المتصفح، وهذا يجعله قوياً جداً لكنه في الوقت نفسه يربك كثيرين عند اتخاذ قرار بسيط: أين أكتب كود الجلب؟ داخل Server Component أم داخل Client Component؟ وهل أحتاج إلى useEffect أصلاً؟ ستجد في هذا الدليل إجابات عملية مدعومة بأمثلة كود قابلة للتشغيل.

أهم ما ستتعلمه في هذا الدليل

  • الفرق الجوهري بين جلب البيانات على الخادم وجلبها في المتصفح، ومتى تختار كل نمط.
  • كيفية استخدام fetch المدمج في Next.js مع خيارات التخزين المؤقت وإعادة التحقق (Revalidation).
  • بناء Route Handlers لتمرير الطلبات إلى API خارجي دون كشف مفاتيحك السرية.
  • استخدام Server Actions لتنفيذ عمليات الكتابة والتحديث مع تحديث الواجهة تلقائياً.
  • معالجة حالات التحميل والأخطاء باستخدام Suspense وملفات loading.js وerror.js.
  • أفضل الممارسات الأمنية والخاصة بالأداء، مع قائمة بالأخطاء الشائعة وكيفية تجنّبها.

ما الذي يجعل جلب البيانات في Next.js مختلفاً عن React التقليدي؟

في تطبيق React تقليدي مبني على Vite أو Create React App، يبدأ المتصفح بتنزيل حزمة JavaScript، ثم يعمل التطبيق، ثم يُرسل طلبات الـ API بعد ذلك. هذه السلسلة تُنتج ما يُعرف بـ Request Waterfall، أي سلسلة انتظار متتالية تظهر للمستخدم على شكل شاشة فارغة أو مؤشر تحميل طويل.

أما في Next.js مع App Router، فالمكوّنات تُنفَّذ على الخادم افتراضياً، ما يعني أنك تستطيع كتابة await مباشرة داخل المكوّن، وسيحصل المتصفح على HTML جاهز يحتوي البيانات فعلياً. هذا الفرق وحده يغيّر شكل التطبيق وأدائه بشكل جذري.

لا حاجة إلى useEffect في معظم الحالات

أكثر سؤال يتكرر بين المطوّرين الجدد على Next.js هو: أين أضع useEffect لجلب البيانات؟ الجواب الصحيح في App Router هو أنك غالباً لا تحتاج إليه إطلاقاً. جلب البيانات على الخادم أبسط، وأسرع، وأكثر أماناً، لأن الكود لا يُرسل إلى المتصفح ولا يُنفَّذ مرتين بسبب Strict Mode.

الفائدة المباشرة على تحسين محركات البحث

عندما تُجلب البيانات على الخادم وتُدمج في HTML، ترى محركات البحث المحتوى الكامل فوراً. هذا مهم للمقالات وصفحات المنتجات وصفحات المتاجر، حيث يعتمد ترتيبك في نتائج البحث على وجود المحتوى في الاستجابة الأولى وليس بعد تنفيذ JavaScript.

فهم نموذج العرض: Server Components مقابل Client Components

قبل الدخول في تفاصيل الكود، يجب أن تكون الصورة واضحة. في App Router كل مكوّن يُعتبر Server Component افتراضياً، ويصبح Client Component فقط عند إضافة التوجيه 'use client' في أعلى الملف.

خصائص Server Components

  • تُنفَّذ على الخادم فقط، ولا تُضاف إلى حزمة JavaScript المُرسلة للمتصفح.
  • يمكنها الوصول مباشرة إلى قواعد البيانات ومتغيرات البيئة السرية.
  • يمكن استخدام async/await بداخلها مباشرةً.
  • لا يمكنها استخدام Hooks مثل useState أو أحداث المستخدم مثل onClick.

خصائص Client Components

  • تُنفَّذ على الخادم للعرض الأولي ثم تُنفَّذ مرة أخرى في المتصفح (Hydration).
  • يمكنها استخدام الحالة والتأثيرات والأحداث والمراجع.
  • لا يمكنها الوصول إلى الأسرار أو إلى قاعدة البيانات مباشرة.
  • تُضيف إلى حجم الحزمة، لذا يُفضّل إبقاؤها صغيرة وفي أوراق الشجرة.

قاعدة عملية للاختيار

اسأل نفسك سؤالاً واحداً: هل أحتاج إلى تفاعل المستخدم أو حالة محلية؟ إن كانت الإجابة نعم، فاستخدم Client Component. وإن كان المكوّن يعرض بيانات فقط، فاتركه Server Component واجلب البيانات داخله.

القاعدة الذهبية: ادفع التوجيه 'use client' إلى أوراق الشجرة قدر الإمكان، وليس إلى جذرها. وضع 'use client' في أعلى الصفحة يحوّل كل ما تحته إلى كود يُرسل للمتصفح.

استخدام fetch المدمج وخيارات التخزين المؤقت في Next.js

Next.js يوسّع دالة fetch الأصلية في المتصفح ويضيف إليها طبقة تخزين مؤقت وإعادة تحقق مبنية على واجهة الويب القياسية. هذا يعني أن الكود الذي تكتبه يبقى متوافقاً مع أي بيئة أخرى، لكنه يكتسب سلوكاً إضافياً داخل Next.js.

إلغاء التخزين المؤقت لجلب بيانات حيّة

إذا كنت تجلب بيانات تتغيّر كل لحظة مثل لوحة تحكم أو محفظة أسعار، فإنك تريد تعطيل التخزين المؤقت بالكامل. الطريقة الأوضح هي تمرير cache: 'no-store'.

const res = await fetch('https://api.example.com/live-prices', {
  cache: 'no-store',
});

const prices = await res.json();

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

إعادة التحقق الزمنية بعد فترة محددة

في كثير من الحالات لا تحتاج إلى بيانات لحظية، بل إلى بيانات حديثة نسبياً. هنا يأتي خيار revalidate الذي ينفّذ نمط stale-while-revalidate.

const res = await fetch('https://api.example.com/articles', {
  next: { revalidate: 300 }, // يُحدَّث كل 5 دقائق
});

المستخدم يحصل فوراً على نسخة مخزّنة، وفي الخلفية يُطلق Next.js طلباً لتحديثها للمستخدمين التالين.

ملاحظة مهمة حول تغيّر السلوك الافتراضي

في Next.js 14 وما قبله كان الافتراضي هو force-cache، أي أن أي fetch بدون خيارات كان يُخزَّن مؤقتاً. أما في Next.js 15 فقد تغيّر الافتراضي إلى no-store ليكون متوافقاً مع سلوك الويب القياسي. لذلك لا تعتمد على الافتراضي، بل صرّح بنيتك بوضوح في كل استدعاء.

التخزين المؤقت المُعنون بالوسوم

الوسوم (Tags) تمنحك تحكماً دقيقاً: يمكنك تخزين مجموعة من الطلبات تحت وسم واحد ثم إبطالها دفعةً واحدة عند حدوث تغيير.

const res = await fetch('https://api.example.com/products', {
  next: { tags: ['products'] },
});

لاحقاً، بعد إتمام عملية إضافة منتج جديد، تستدعي revalidateTag('products') فيُبطَل كل ما يتعلق بهذا الوسم فوراً.

جلب البيانات داخل Server Components خطوة بخطوة

هذا هو النمط الذي ستحتاجه في الغالبية العظمى من صفحاتك. الفكرة أن المكوّن نفسه يصبح دالة غير متزامنة تجلب البيانات وترسمها.

// app/posts/page.js
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 60 },
  });

  if (!res.ok) {
    throw new Error('فشل جلب المقالات: ' + res.status);
  }

  return res.json();
}

export default async function PostsPage() {
  const posts = await getPosts();

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

النتيجة هي صفحة HTML تحتوي قائمة المقالات كاملة قبل وصول أي JavaScript إلى المتصفح. لا يوجد مؤشر تحميل، ولا وميض، ولا طلب ثانٍ من العميل.

التحقق من حالة الاستجابة قبل تحليلها

لاحظ وجود الفحص if (!res.ok). هذه نقطة يغفل عنها كثيرون، فالـ fetch لا يرفض الوعد (Promise) عند استجابة 404 أو 500، بل يجب عليك فحص الحالة يدوياً. إذا تجاهلت هذا الفحص، ستحاول تحليل نص خطأ كأنه JSON وسيظهر لك استثناء غامض.

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

الخطأ الأكثر تكلفة في الأداء هو كتابة الطلبات تسلسلياً رغم استقلالها عن بعضها. استخدم Promise.all لتشغيلها معاً.

export default async function DashboardPage() {
  const [user, orders, stats] = await Promise.all([
    getUser(),
    getOrders(),
    getStats(),
  ]);

  return <Dashboard user={user} orders={orders} stats={stats} />;
}

بالجلب التسلسلي كان زمن الانتظار يساوي مجموع الأزمنة، أما الآن فهو يساوي أطول طلب فقط. الفرق في صفحة تحتوي خمسة طلبات بمعدل 200 مللي ثانية لكل طلب قد يصل إلى أكثر من 800 مللي ثانية.

الوصول إلى ترويسات الطلب والكوكيز

أحياناً تحتاج إلى تمرير توكن المستخدم إلى الـ API الخارجي. يمكنك قراءة الترويسات داخل Server Component باستخدام دالة headers.

import { headers } from 'next/headers';

export default async function ProfilePage() {
  const headersList = await headers();
  const token = headersList.get('x-user-token');

  const res = await fetch('https://api.example.com/me', {
    headers: { Authorization: 'Bearer ' + token },
    cache: 'no-store',
  });

  const profile = await res.json();
  return <Profile data={profile} />;
}

استخدام headers أو cookies يحوّل الصفحة تلقائياً إلى عرض ديناميكي، لأن الناتج يعتمد على الطلب نفسه ولا يمكن تخزينه مسبقاً.

جلب البيانات في Client Components: useEffect أم SWR أم React Query؟

هناك حالات مشروعة تحتاج فيها إلى الجلب من المتصفح: البحث الفوري، التمرير اللانهائي، تحديث البيانات كل بضع ثوانٍ، أو الاعتماد على قيمة موجودة في حالة العميل فقط.

الطريقة الأساسية باستخدام useEffect

هذه الطريقة مناسبة للتعلم أو للحالات البسيطة جداً، لكنها تتطلب منك إدارة كل شيء يدوياً.

'use client';

import { useEffect, useState } from 'react';

export default function PostsClient() {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    async function load() {
      try {
        const res = await fetch('/api/posts', { signal: controller.signal });
        if (!res.ok) throw new Error('فشل الطلب');
        setPosts(await res.json());
      } catch (err) {
        if (err.name !== 'AbortError') setError(err.message);
      } finally {
        setLoading(false);
      }
    }

    load();
    return () => controller.abort();
  }, []);

  if (loading) return <p>جارٍ التحميل...</p>;
  if (error) return <p>حدث خطأ: {error}</p>;

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

لاحظ استخدام AbortController في دالة التنظيف. هذا يمنع تحديث الحالة بعد إلغاء تحميل المكوّن، وهو سبب شائع لتحذيرات React.

استخدام SWR لتبسيط الكود

مكتبة SWR من فريق Vercel تختصر كل الكود السابق في بضعة أسطر وتضيف مزايا مجانية مثل إعادة الجلب عند العودة إلى التبويب وإلغاء التكرار.

'use client';

import useSWR from 'swr';

const fetcher = (url) => fetch(url).then((res) => res.json());

export default function PostsList() {
  const { data, error, isLoading } = useSWR('/api/posts', fetcher, {
    revalidateOnFocus: false,
  });

  if (isLoading) return <p>جارٍ التحميل...</p>;
  if (error) return <p>تعذّر جلب البيانات.</p>;

  return (
    <ul>
      {data.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

خيار revalidateOnFocus: false مفيد عندما لا تريد طلباً جديداً في كل مرة يعود فيها المستخدم إلى التبويب، وهو سلوك افتراضي قد يزعج مستخدمي لوحات التحكم.

متى تختار React Query؟

إذا كان تطبيقك يعتمد بشكل كثيف على حالة الخادم من جهة العميل، مثل صفحات إدارة معقدة فيها ترقيم وتصفية وتحسينات تفاؤلية، فإن مكتبة مثل TanStack Query تمنحك طبقة إدارة أقوى من SWR: أدوات تطوير ممتازة، وتحكم دقيق في إبطال الاستعلامات، ودعم أفضل للتحديثات التفاؤلية المتشابكة.

تحذير مهم قبل أن تبدأ

  • لا تجلب بيانات لا تحتاجها في العميل لمجرد أنك اعتدت على ذلك في React.
  • لا تستدعِ API خارجياً بمفتاح سري من داخل Client Component، لأن المفتاح سيظهر في تبويب الشبكة.
  • لا تكرّر نفس الطلب في أكثر من مكوّن دون طبقة تخزين مؤقت مثل SWR أو React Query.

بناء Route Handlers للتواصل مع APIs الخارجية وحماية مفاتيحك

Route Handlers هي البديل الحديث لـ API Routes في App Router، وتُكتب داخل مجلد app/api في ملف باسم route.js. أهم استخدام لها مع جلب البيانات هو بناء طبقة وسيطة (Proxy) تحمي مفاتيح الـ API.

لماذا نحتاج إلى طبقة وسيطة؟

تخيّل أنك تستخدم خدمة ذكاء اصطناعي أو بوابة دفع بمفتاح سري. لو استدعيت الخدمة مباشرة من المتصفح، سيتمكن أي زائر من نسخ المفتاح واستهلاك رصيدك. الحل هو أن يطلب المتصفح من مسار داخلي في تطبيقك، ويقوم الخادم بدوره بالاتصال بالخدمة مع إضافة المفتاح.

// app/api/posts/route.js
import { NextResponse } from 'next/server';

export async function GET(request) {
  const { searchParams } = new URL(request.url);
  const page = searchParams.get('page') ?? '1';

  const res = await fetch(
    `https://api.example.com/posts?page=${page}&limit=10`,
    {
      headers: { Authorization: `Bearer ${process.env.API_TOKEN}` },
      next: { revalidate: 30 },
    }
  );

  if (!res.ok) {
    return NextResponse.json(
      { error: 'تعذّر جلب البيانات من المصدر' },
      { status: res.status }
    );
  }

  const data = await res.json();
  return NextResponse.json(data);
}

بهذا الشكل يستدعي المتصفح /api/posts?page=2 فقط، بينما يبقى المفتاح السري داخل بيئة الخادم ولا يظهر أبداً في أدوات المطوّر.

معالجة طلبات الكتابة مع التحقق من المدخلات

لا يكفي أن تنقل الطلب كما هو؛ يجب أن تتحقق من المدخلات وإلا حوّلت تطبيقك إلى بوابة مفتوحة لمن يريد استغلال الخدمة.

export async function POST(request) {
  const body = await request.json();

  if (!body || typeof body.title !== 'string' || body.title.trim() === '') {
    return NextResponse.json(
      { error: 'العنوان مطلوب ويجب أن يكون نصاً غير فارغ' },
      { status: 400 }
    );
  }

  const res = await fetch('https://api.example.com/posts', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${process.env.API_TOKEN}`,
    },
    body: JSON.stringify({ title: body.title.trim() }),
  });

  if (!res.ok) {
    return NextResponse.json(
      { error: 'فشل إنشاء المقال' },
      { status: 502 }
    );
  }

  return NextResponse.json(await res.json(), { status: 201 });
}

التحقق من النوع وطول النص قبل إرساله خطوة بسيطة تحميك من تمرير بيانات غير صالحة إلى خدمة خارجية قد ترسل بدورها رسائل خطأ غير مفهومة للمستخدم.

قراءة معاملات المسار الديناميكي

يمكنك أيضاً بناء مسارات ديناميكية مثل app/api/posts/[id]/route.js وقراءة المعرّف من الوسيط الثاني. هذه طريقة نظيفة لبناء واجهة داخلية متسقة يستخدمها العميل بسهولة.

Server Actions: الكتابة والتحديث بدون نقاط نهاية إضافية

Server Actions هي دوال تعمل على الخادم لكن يمكن استدعاؤها من العميل مباشرة كأنها دوال عادية، دون كتابة Route Handler ولا إدارة عنوان URL ولا تحليل يدوي للاستجابة.

تعريف إجراء على الخادم

أضف التوجيه 'use server' في أعلى الملف، وستتحول كل الدوال المُصدَّرة فيه إلى إجراءات خادمية.

// app/actions.js
'use server';

import { revalidateTag } from 'next/cache';

export async function addPost(formData) {
  const title = formData.get('title');

  if (!title || title.trim() === '') {
    return { error: 'العنوان مطلوب' };
  }

  const res = await fetch('https://api.example.com/posts', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ title: title.trim() }),
  });

  if (!res.ok) {
    return { error: 'فشل إنشاء المقال' };
  }

  revalidateTag('posts');
  return { success: true };
}

الاستدعاء revalidateTag('posts') هو ما يجعل الواجهة تتحدّث تلقائياً بعد نجاح العملية، لأن كل الطلبات الموسومة بهذا الوسم تُبطل ويُعاد جلبها.

استخدام الإجراء داخل نموذج في العميل

يمكنك تمرير الإجراء مباشرة إلى خاصية action في النموذج، مع استخدام useActionState لقراءة نتيجة التنفيذ ومعرفة هل العملية قيد التنفيذ.

'use client';

import { useActionState } from 'react';
import { addPost } from './actions';

export default function NewPostForm() {
  const [state, formAction, pending] = useActionState(addPost, null);

  return (
    <form action={formAction}>
      <input name="title" placeholder="عنوان المقال" />
      <button type="submit" disabled={pending}>
        {pending ? 'جارٍ الحفظ...' : 'حفظ'}
      </button>
      {state?.error && <p>{state.error}</p>}
    </form>
  );
}

هذا النمط يقلل الكود بشكل كبير: لا حاجة إلى fetch في العميل، ولا إلى إدارة حالة التحميل يدوياً، ولا إلى endpoint جديد. النموذج يعمل حتى لو تعطّل JavaScript جزئياً، وهو تحسين حقيقي في تجربة الاستخدام.

متى تختار Server Action ومتى تختار Route Handler؟

  1. اختر Server Action عندما تكون العملية مرتبطة مباشرة بواجهة المستخدم في تطبيقك، مثل نموذج تسجيل أو زر إعجاب.
  2. اختر Route Handler عندما تحتاج إلى واجهة عامة يستخدمها تطبيق جوال أو طرف ثالث، أو عندما تريد تحكماً كاملاً في الترويسات ورموز الحالة والتخزين المؤقت.

التعامل مع حالات التحميل والأخطاء في جلب البيانات

تجربة المستخدم لا تكتمل إلا بمعالجة الحالتين: ماذا يرى المستخدم أثناء الانتظار، وماذا يرى عندما يفشل الطلب؟

ملف loading.js للتحميل التلقائي

بمجرد إضافة ملف loading.js داخل أي مسار، سيُعرض تلقائياً أثناء انتظار جلب البيانات في ذلك المسار، دون كتابة أي كود إضافي.

// app/posts/loading.js
export default function Loading() {
  return <p>جارٍ تحميل المقالات...</p>;
}

ما يحدث فعلياً هو أن Next.js يغلّف الصفحة في حدود Suspense ويعرض هذا الملف كواجهة بديلة حتى تجهز البيانات.

ملف error.js لالتقاط الأخطاء

يجب أن يكون ملف الخطأ مكوّناً عميلياً، لأنه يستخدم دالة إعادة المحاولة.

// app/posts/error.js
'use client';

export default function Error({ error, reset }) {
  return (
    <>
      <h3>حدث خطأ أثناء جلب المقالات</h3>
      <p>{error.message}</p>
      <button onClick={() => reset()}>إعادة المحاولة</button>
    </>
  );
}

ملف الخطأ يلتقط الأخطاء التي يرفعها المكوّن، بما فيها الخطأ الذي تُطلقه أنت يدوياً عند فشل الاستجابة. لهذا كان الفحص if (!res.ok) مهماً: يمنحك رسالة مفهومة بدل استثناء غامض من مُحلّل JSON.

التدفّق التدريجي باستخدام Suspense

عندما تحتوي الصفحة على أجزاء بعضها سريع وبعضها بطيء، يمكنك عرض الأجزاء السريعة فوراً وترك الأبطأ يُبثّ لاحقاً.

import { Suspense } from 'react';

export default function Page() {
  return (
    <>
      <h1>لوحة التحكم</h1>
      <Suspense fallback={<p>جارٍ تحميل الإحصائيات...</p>}>
        <SlowStats />
      </Suspense>
    </>
  );
}

هذا الأسلوب يقلل Time To First Byte لأن الخادم يبدأ بإرسال HTML قبل أن تنتهي كل الطلبات، ويشعر المستخدم بأن الصفحة حيّة وليست متجمدة.

إعادة التحقق وتحديث البيانات تلقائياً

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

إبطال الوسوم بعد الكتابة

هذا هو النمط الأنسب لمعظم التطبيقات: تخزين الطلبات بوسوم، ثم إبطال الوسم المحدد بعد أي عملية كتابة ناجحة.

import { revalidateTag } from 'next/cache';

export async function updateProduct(id, data) {
  await fetch(`https://api.example.com/products/${id}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  });

  revalidateTag('products');
}

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

إعادة التحقق على مستوى المسار

يمكنك أيضاً تصدير ثابت من ملف الصفحة نفسه لضبط سلوك المسار كاملاً.

// app/blog/page.js
export const revalidate = 3600; // يُعاد التوليد كل ساعة

export default async function BlogPage() {
  const posts = await getPosts();
  return <PostList posts={posts} />;
}

هذا الخيار مفيد لصفحات المحتوى التي لا تتغير باستمرار، مثل المدونات وصفحات الأسئلة الشائعة.

تخزين نتائج قاعدة البيانات

إذا كنت تجلب البيانات من قاعدة بيانات مباشرة بدل HTTP، يمكنك الاستفادة من نفس آلية التخزين المؤقت باستخدام unstable_cache.

import { unstable_cache } from 'next/cache';

export const getPopularPosts = unstable_cache(
  async () => db.post.findMany({ take: 10 }),
  ['popular-posts'],
  { revalidate: 300, tags: ['posts'] }
);

النتيجة أن الاستعلام الثقيل لا يُنفَّذ إلا مرة واحدة كل خمس دقائق، مع إمكانية إبطاله فوراً عبر الوسم عند إضافة مقال جديد.

تحسين الأداء عند جلب البيانات من APIs

الأداء في جلب البيانات لا يعتمد على سرعة الشبكة وحدها، بل على شكل الكود الذي تكتبه. إليك أهم الممارسات التي تُحدث فرقاً ملموساً.

القضاء على الشلالات المتسلسلة

كل await متتالٍ لا يعتمد على سابقه يُضيف زمن انتظاراً كاملاً. راجع مسار العرض لديك وابحث عن أي طلبات يمكن تشغيلها معاً بواسطة Promise.all.

تحديد مهلة زمنية للطلبات الخارجية

الـ API الخارجي قد يتوقف عن الاستجابة دون أن يغلق الاتصال، فتتعلق صفحتك إلى الأبد. الحل هو تقييد المهلة.

try {
  const res = await fetch('https://api.example.com/slow', {
    signal: AbortSignal.timeout(5000),
  });
  return await res.json();
} catch (err) {
  return { error: 'انتهت مهلة الاتصال بالخدمة' };
}

مهلة خمس ثوانٍ قيمة معقولة لمعظم الخدمات، ويمكن تعديلها حسب حساسية الصفحة.

تقليل حجم JavaScript المُرسل للمتصفح

كل مكوّن يحمل 'use client' يُضاف إلى الحزمة. إذا كانت صفحتك تجلب البيانات على الخادم إلا جزءاً صغيراً تفاعلياً، فافصل ذلك الجزء في ملف مستقل واترك الباقي خادمياً.

الترقيم والتحميل التدريجي

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

الاستفادة من التحسين التلقائي للطلبات المتكررة

Next.js يزيل تكرار الطلبات المتطابقة داخل دورة العرض الواحدة. لذا إذا استدعت ثلاثة مكوّنات نفس الـ API بنفس الخيارات، فسيُنفَّذ الطلب مرة واحدة فقط على الشبكة. لا تحاول بناء طبقة إلغاء تكرار يدوية قبل التأكد من حاجتك إليها.

الأمان عند جلب البيانات من APIs في Next.js

الأمان هنا ليس رفاهية، لأن أهم خطأ في هذا المجال قد يكلفك فاتورة ضخمة أو تسريب بيانات مستخدمين.

افصل بين متغيرات البيئة العامة والسرية

أي متغير يبدأ بـ NEXT_PUBLIC_ سيُدمج في حزمة المتصفح ويستطيع أي زائر قراءته. لا تضع فيه مفاتيح سرية أبداً.

# .env.local
NEXT_PUBLIC_API_URL=https://api.example.com
API_TOKEN=sk_live_xxxxxxxxxxxxxxxx

الأول يمكن استخدامه من المكوّنات العميلية والخادمية، أما الثاني فلا يُستخدم إلا في Server Components وRoute Handlers وServer Actions.

تحقق من المدخلات دائماً

أي بيانات تأتي من العميل غير موثوقة، حتى لو كان الحقل مخفياً في الواجهة. استخدم مكتبة تحقق مثل Zod للتحقق من الشكل والأنواع قبل تمرير البيانات إلى الخدمة الخارجية.

قيّد الوجهات التي يطلبها الخادم

إذا بنيت مساراً يقبل عنوان URL من المستخدم ثم يجلبه الخادم، فأنت تفتح باباً لهجمات SSRF حيث يمكن استهداف خدمات داخلية في شبكتك. قيّد القيم المسموح بها بقائمة بيضاء واضحة.

تجنّب تسريب رسائل الخطأ الخام

لا تُرجع نص استجابة الخدمة الخارجية كما هو إلى العميل، فقد يحتوي على تفاصيل داخلية. أرجع رسالة عامة وسجّل التفاصيل في الخادم.

فعّل حدود المعدلات

إذا كنت تستخدم مفتاح خدمة مدفوع، فقد يكون من السهل استنزاف رصيدك عبر تكرار الطلبات. أضف Rate Limiting على مساراتك الداخلية، خاصة مسارات المصادقة والإرسال.

الأخطاء الشائعة في جلب البيانات وكيف تتجنبها

  • جلب كل شيء في العميل: يضخّم الحزمة ويؤخر ظهور المحتوى. انقل ما يمكن نقله إلى الخادم.
  • تجاهل فحص res.ok: يقودك إلى أخطاء JSON غامضة بدل رسائل واضحة.
  • نسيان التخزين المؤقت في Next.js 15: بعد تغيّر الافتراضي إلى no-store، قد تجد تطبيقك أبطأ لأنه يعيد الجلب في كل طلب. صرّح بـ revalidate عند الحاجة.
  • وضع 'use client' في أعلى الصفحة: يحوّل شجرة كاملة إلى كود عميلي بلا داعٍ.
  • استخدام useEffect داخل Server Component: لن يعمل، لأن الخادم لا يملك دورة حياة المتصفح.
  • تمرير المفاتيح السرية إلى المكوّنات العميلية: المفتاح سيظهر لأي مستخدم يفتح أدوات المطوّر.
  • بناء شلالات طلبات: كل await متتالٍ مستقل يضيف زمناً يمكن توفيره.
  • عدم معالجة مهلة الطلبات الخارجية: صفحة معلّقة عند تعطّل خدمة بعيدة.
  • إبطال التخزين المؤقت للتطبيق كاملاً: استخدم الوسوم لإبطال الجزء المتأثر فقط.
  • تعريف الإجراءات الخادمية داخل ملفات المكوّنات العميلية: يجب أن تكون في ملف منفصل يبدأ بـ 'use server'.

أفضل الممارسات لتنظيم طبقة جلب البيانات

  1. افصل منطق الجلب عن العرض: ضع دوال الجلب في ملف مثل lib/api.js واستدعها من الصفحات، فيسهل اختبارها وتغييرها لاحقاً.
  2. وحّد معالجة الأخطاء: أنشئ دالة مساعدة تُغلّف fetch وتتحقق من الحالة وترفع خطأً موحداً.
  3. عرّف أنواع البيانات: عند استخدام TypeScript، حدّد واجهة لكل كائن قادم من الـ API لتكتشف الأخطاء وقت الترجمة لا وقت التشغيل.
  4. اختر استراتيجية تخزين مؤقت لكل مسار على حدة: البيانات اللحظية بلا تخزين، والمحتوى شبه الثابت بفترة إعادة تحقق طويلة.
  5. اعرض حالات فراغ واضحة: القائمة الفارغة ليست خطأ، لكنها تحتاج رسالة تفهمها.
  6. استخدم الوسوم بذكاء: وسم واحد لكل نوع مورد مثل posts وproducts وuser:123.
  7. قِس قبل أن تُحسّن: استخدم أدوات مثل Lighthouse وأدوات Next.js المدمجة لمعرفة المسار البطيء فعلياً.
  8. وثّق الـ API التي تعتمد عليها: ملاحظة صغيرة عن نقاط النهاية ومعدلات السماح توفر ساعات على فريقك لاحقاً.

مثال على دالة جلب موحّدة وقابلة لإعادة الاستخدام

هذا النمط البسيط يجمع كل الممارسات السابقة في مكان واحد: تحقق من الاستجابة، ومهلة زمنية، وخيارات تخزين مؤقت افتراضية واضحة.

// lib/api.js
const BASE_URL = process.env.NEXT_PUBLIC_API_URL;

export async function apiFetch(path, options = {}) {
  const res = await fetch(BASE_URL + path, {
    signal: AbortSignal.timeout(8000),
    ...options,
    headers: {
      'Content-Type': 'application/json',
      ...(options.headers || {}),
    },
  });

  if (!res.ok) {
    throw new Error(`Request failed with status ${res.status}`);
  }

  return res.json();
}

الآن يمكن لأي صفحة أن تستدعي apiFetch('/posts') وتحصل على نفس السلوك المتوقع دون تكرار الكود.

متى تستخدم كل نمط؟ دليل قرار عملي

  • صفحة محتوى عامة أو مقال أو منتج: جلب داخل Server Component مع revalidate مناسب. أفضل أداء وأفضل نتيجة لمحركات البحث.
  • لوحة تحكم خاصة بالمستخدم: جلب داخل Server Component مع cache: 'no-store'، لأن البيانات شخصية وتتغير.
  • بحث فوري مع كتابة المستخدم: Client Component مع تأخير بسيط (Debounce) وطلب إلى Route Handler أو Server Action.
  • تمرير الطلبات إلى خدمة بمفتاح سري: Route Handler دائماً.
  • نموذج إرسال أو زر إعجاب: Server Action مع revalidateTag.
  • تحديث دوري لحالة متغيرة: Client Component مع SWR أو TanStack Query وخاصية إعادة الجلب الدوري.

Frequently Asked Questions About جلب البيانات من APIs في Next.js

هل يجب أن أستخدم useEffect لجلب البيانات في Next.js؟

في معظم الحالات لا. إذا كنت تستخدم App Router، فاجلب البيانات داخل Server Component باستخدام await مباشرة. استخدم useEffect فقط عندما يعتمد الطلب على حالة موجودة في العميل ولا يمكن معرفتها على الخادم.

ما الفرق بين cache: 'no-store' و revalidate؟

خيار no-store يطلب من Next.js عدم تخزين النتيجة إطلاقاً وإعادة تنفيذ الطلب في كل مرة، وهو مناسب للبيانات اللحظية. أما revalidate فيخزّن النتيجة لفترة محددة ويعيد توليدها بعدها، وهو مناسب للمحتوى الذي يتغير كل بضع دقائق.

هل يمكن استخدام Server Actions مع API خارجي؟

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

كيف أحمي مفتاح الـ API من الظهور في المتصفح؟

لا تستدعِ الخدمة التي تحتاج المفتاح من Client Component. أنشئ Route Handler أو Server Action يتصل بالخدمة، واحفظ المفتاح في متغير بيئة بدون بادئة NEXT_PUBLIC_، وليتواصل المتصفح مع مسارك الداخلي فقط.

لماذا أصبح تطبيقي أبطأ بعد الترقية إلى Next.js 15؟

لأن السلوك الافتراضي لـ fetch تغيّر من التخزين المؤقت إلى عدم التخزين. الحل هو تحديد استراتيجية التخزين صراحةً في الطلبات التي تقبل بيانات قديمة نسبياً، مثل إضافة next: { revalidate: 60 }.

هل يمكن الجمع بين الجلب على الخادم والجلب في العميل في نفس الصفحة؟

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

كيف أتعامل مع فشل الـ API دون أن تتعطل الصفحة كاملة؟

استخدم حدود Suspense حول الجزء الذي يعتمد على البيانات، وفعّل ملف error.js في المسار. بهذا يفشل الجزء المعني وحده ويبقى باقي المحتوى قابلاً للاستخدام مع زر إعادة محاولة.

خطواتك التالية لتطبيق ما تعلمته

المعرفة الحقيقية تظهر عند التطبيق، لذا ابدأ بخطوة صغيرة ووسّعها تدريجياً. الطريق العملي المقترح هو:

  1. حوّل صفحة واحدة في مشروعك الحالي من الجلب في العميل إلى الجلب داخل Server Component، وقس الفرق في زمن الظهور.
  2. أضف استراتيجية تخزين مؤقت واضحة لكل مسار: فترة إعادة تحقق للمحتوى العام، وعدم تخزين للبيانات الشخصية.
  3. أنشئ Route Handler واحداً لكل خدمة خارجية تستخدم مفتاحاً سرياً، ولا تستدعِ تلك الخدمة من المتصفح بعد اليوم.
  4. استبدل نموذج الإرسال في تطبيقك بـ Server Action مع revalidateTag ولاحظ كم سطر كود حذفت.
  5. أضف ملفات loading.js وerror.js في المسارات الرئيسية إن لم تكن موجودة، ثم اختبر تجربة الفشل والانتظار يدوياً.
  6. راجع شجرة المكوّنات وابحث عن أي 'use client' يمكن دفعه إلى الأسفل أو حذفه بالكامل.

بهذه الخطوات الست ستتحول من كتابة كود يعمل إلى بناء بنية بيانات واضحة يعرف فيها كل طلب مكانه الصحيح، وهذا هو الفرق الحقيقي بين مشروع Next.js قابل للنمو ومشروع يتحول إلى عبء صيانة. التزم بمبدأ واحد: اجلب البيانات أقرب ما يمكن إلى مصدرها، واعرضها في أبعد نقطة ممكنة عن المتصفح.

#جلب البيانات من apis في next.js #next.js #app router #server components #client components #fetch api #server actions #route handlers #تخزين مؤقت #إعادة التحقق #react #تطوير الويب

اشترك في نشرتنا البريدية

12k+

المشتركون

أسبوعيًا

التكرار

مجاني

دائمًا