توقف عن استخدام useEffect لجلب البيانات: دليل المهندس الأول للمكونات الخادمية

عادة جلب البيانات في useEffect صعبة الترك
عندما ظهرت React Hooks، أصبح useEffect المكان الافتراضي لجلب البيانات. علّمتها الدروس التعليمية. كافأها Stack Overflow. تعلم كل مطور مبتدئ وضع fetch داخل useEffect مع مصفوفة تبعيات فارغة وحالة تحميل. لقد نجح الأمر. لكنه كان أيضًا بداية لقائمة طويلة من المشكلات لا نزال ننظفها.
قضيت العام الماضي في نقل تطبيق Next.js من Pages Router إلى App Router. لم يكن التغيير الأكبر هو هيكل الملفات أو اصطلاحات التوجيه الجديدة. بل كان تعلم التوقف عن اللجوء إلى useEffect في كل مرة أحتاج فيها إلى بيانات. تغيرت المكونات الخادمية القاعدة الافتراضية. فهي لا تجعل جلب البيانات من العميل مهملاً، لكنها تجعله استثناءً لا قاعدة.
يتناول هذا المقال متى نجلب البيانات على الخادم، ومتازل نحتاج العميل، وكيف نتجنب أسوأ ما في العالمين.
ما الخطأ في جلب البيانات عبر useEffect
يبدو جلب البيانات داخل useEffect بسيطًا حتى تحاول جعله قويًا. ستحتاج إلى حالة تحميل، وحالة خطأ، ومنطق إلغاء، ومنطق إعادة المحاولة، وطريقة لمنع ذاكرة التخزين المؤقت من أن تصبح قديمة. ثم تضاعف ذلك في كل مكون يحتاج إلى بيانات. وبسرعة، تصبح تطبيقك مجموعة من مديري البيانات الصغار الذين يتصرفون بشكل مختلف قليلاً.
المشكلات الأكبر معمارية. يُرسل جلب البيانات من جانب العميل JavaScript إلى المتصفح، ويعمل بعد الرسم الأولي، ويحجب التفاعل أثناء حل طلب الشبكة، ويخلق شلالات عندما تجلب المكونات الأصلية البيانات قبل أن تتمكن المكونات الفرعية حتى من البدء. كما أنه يربط مصدر البيانات بشجرة المكونات، مما يجعل الاختبار وإعادة الاستخدام أصعب مما ينبغي.
هناك حالات مشروعة للجلب من جانب العميل. التفاعلات الخاصة بالمستخدم، والتحديثات في الوقت الفعلي، وأي شيء يعتمد على واجهات برمجة تطبيقات متاحة فقط في المتصفح تنتمي إلى العميل. لكن جلب البيانات الافتراضي للصفحة يجب ألا يبدأ تقريبًا أبدًا في useEffect.
المكونات الخادمية تغير القاعدة الافتراضية
في App Router من Next.js، تكون المكونات خادمية افتراضيًا. تعمل على الخادم، ويمكن أن تكون غير متزامنة، ويمكنها جلب البيانات مباشرة في وقت العرض. يتم إرسال النتيجة إلى المتصفح كـ HTML، مما يعني أن المستخدم يرى المحتوى دون انتظار تشغيل JavaScript وإجراء جولة أخرى.
// app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
interface PostPageProps {
params: Promise<{ slug: string }>;
}
export default async function PostPage({ params }: PostPageProps) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) {
notFound();
}
return (
<article>
<h1>{post.title}</h1>
<p>{post.excerpt}</p>
<PostBody body={post.body} />
</article>
);
}يقوم هذا المكون بشيء يبدو غريبًا إذا كنت قادمًا من Pages Router. إنه ينتظر البيانات أثناء العرض. لا يوجد useState، ولا useEffect، ولا مؤشر تحميل يتم عرضه بواسطة المكون نفسه. تتم معالجة حالة التحميل بواسطة loading.tsx في نفس القطاع، ويتم التعامل مع الأخطاء بواسطة error.tsx. يعرف المكون فقط كيفية تحويل البيانات إلى ترميز.
التغيير في النموذج الذهني هو: المكون ليس تطبيقًا صغيرًا يدير دورة حياته الخاصة. إنه دالة تحول الطلب إلى HTML. جلب البيانات هو جزء من هذا التحول، وليس تأثيرًا جانبيًا للتركيب.
طبقة التخزين المؤقت هي الفوز الحقيقي
لا يتعلق الجلب من جانب الخادم في Next.js بمكان تشغيل الكود فقط. بل يتعلق أيضًا بمعاني التخزين المؤقت التي تأتي معه. تم توسيع واجهة برمجة تطبيقات fetch في المكونات الخادمية بخيار cache يتيح لك تحديد ما إذا كان يجب تخزين الطلب مؤقتًا، ومدة صلاحيته، ومتى يجب إعادة التحقق من صحته.
// مخزن مؤقت افتراضيًا طوال عمر الطلب
const posts = await fetch("https://api.example.com/posts", {
next: { revalidate: 3600 },
}).then((res) => res.json());يخبر خيار next.revalidate Next.js بتخزين الاستجابة مؤقتًا على مستوى ذاكرة التخزين المؤقت للبيانات لمدة ساعة واحدة. هذا ليس نفسه ذاكرة التخزين المؤقت للمتصفح. إنها ذاكرة تخزين مؤقت من جانب الخادم مشتركة عبر الطلبات، مما يعني أن واجهة برمجة التطبيقات المنبثقة لا يتم ضربها إلا مرة واحدة في الساعة لكل عنوان URL فريد، بغض النظر عن عدد المستخدمين الذين يزورون الصفحة.
هناك ثلاث ذاكرات تخزين مؤقت عليك فهمها:
- ذاكرة التخزين المؤقت للبيانات: تخزن استجابات
fetchعبر الطلبات والنشرات. - ذاكرة التخزين المؤقت للمسار: تخزن مقاطع المسار المحملة مسبقًا في المتصفح للتنقل السريع.
- ذاكرة التخزين المؤقت الكاملة للمسار: تخزن HTML المعروض للمسارات الثابتة في وقت البناء أو بعد إعادة التحقق.
عندما تعمل هذه الطبقات معًا، تحصل على صفحات سريعة عند الزيارة الأولى وسريعة جدًا تقريبًا عند التنقل اللاحق. عندما تتعارض مع بعضها البعض، تحصل على سلوك محير حيث لا تظهر التحديثات أو تبقى البيانات القديمة لفترة أطول من المتوقع.
مكان تناسب Suspense والبث
لا يمكن انتظار كل عملية جلب بيانات قبل عرض الصفحة. بعض البيانات بطيئة أو ثانوية أو تعتمد على تفاعل المستخدم. وهنا تأتي حدود Suspense. تلف الجزء غير المتزامن من الشجرة في حد Suspense وتسمح لـ Next.js ببث بقية الصفحة إلى المتصفح أثناء حل البيانات البطيئة.
import { Suspense } from "react";
import { PostContent } from "./PostContent";
import { RelatedArticles } from "./RelatedArticles";
import { RelatedSkeleton } from "./RelatedSkeleton";
export default async function PostPage({ params }: PostPageProps) {
const { slug } = await params;
return (
<article>
<PostContent slug={slug} />
<Suspense fallback={<RelatedSkeleton />}>
<RelatedArticles slug={slug} />
</Suspense>
</article>
);
}التفصيل المهم هو أن RelatedArticles هو أيضًا مكون خادمي. يمكن أن يكون غير متزامن ويجلب بياناته الخاصة. يمنح حد Suspense العرض التدريجي دون نقل الجلب إلى العميل. يرى المستخدم المحتوى الرئيسي فورًا والمقالات ذات الصلة عندما تكون جاهزة.
من الصعب تكرار هذا النمط بشكل نظيف باستخدام useEffect. ستحتاج إلى تنسيق حالات التحميل عبر مكونات متعددة، وإدارة عناصر نائبة، والأمل ألا يرسم المتصفح تسلسلًا محرجًا من أدوات التحميل.
متى لا يزال جلب البيانات من جانب العميل منطقيًا
المكونات الخادمية ليست دينًا. هناك أماكن يكون فيها العميل هو المكان الصحيح لجلب البيانات.
- تفاعلات المستخدم التي تغير البيانات: البحث أثناء الكتابة، والتصفية، والترقيم الصفحي، والفرز. تنتمي هذه إلى مكونات العميل باستخدام مكتبة مثل TanStack Query أو SWR.
- البيانات في الوقت الفعلي: لوحات المعلومات المباشرة، والإشعارات، والدردشة. لا يمكن للخادم دفع التحديثات إلى عرض ثابت.
- واجهات برمجة تطبيقات المتصفح فقط: الموقع الجغرافي، والحافظة، والتخزين المحلي، وBluetooth. إذا كانت البيانات تعتمد على المتصفح، فاجلبها من جانب العميل.
- البيانات المخصصة بعد الترطيب: تفضيلات المستخدم، والعناصر التي شاهدها مؤخرًا، وتعيينات اختبار A/B.
قاعدة الإبهام بسيطة. إذا كانت البيانات مطلوبة لعرض الصفحة الأولية، فاجلبها على الخادم. إذا كانت البيانات تنتج عن تفاعل أو موجودة فقط في المتصفح، فاجلبها على العميل.
ترقيم الصفحات مع معلمات الاستعلام
إحدى أولى الأسئلة التي تطرحها الفرق عند الانتقال إلى المكونات الخادمية هي كيفية التعامل مع ترقيم الصفحات. في Pages Router، عاش ترقيم الصفحات عادةً في useState أو useRouter. في App Router، عنوان URL هو مصدر الحقيقة، وتتلقى المكونات الخادمية searchParams كخاصية.
// app/blog/page.tsx
interface BlogPageProps {
searchParams: Promise<{ page?: string; limit?: string }>;
}
export default async function BlogPage({ searchParams }: BlogPageProps) {
const { page = "1", limit = "10" } = await searchParams;
const pageNum = Math.max(1, parseInt(page, 10) || 1);
const limitNum = Math.min(50, Math.max(1, parseInt(limit, 10) || 10));
const { posts, total } = await getPosts({ page: pageNum, limit: limitNum });
const totalPages = Math.ceil(total / limitNum);
return (
<main>
<PostGrid posts={posts} />
<PaginationControls
page={pageNum}
totalPages={totalPages}
limit={limitNum}
/>
</main>
);
}المكون PaginationControls هو مكون عميل لأنه يتعامل مع النقرات ويحدث عنوان URL. إنه لا يجلب البيانات. إنه يبني الروابط فقط.
"use client";
import { usePathname, useSearchParams } from "next/navigation";
import Link from "next/link";
interface PaginationControlsProps {
page: number;
totalPages: number;
limit: number;
}
export function PaginationControls({
page,
totalPages,
limit,
}: PaginationControlsProps) {
const pathname = usePathname();
const searchParams = useSearchParams();
const buildHref = (nextPage: number) => {
const params = new URLSearchParams(searchParams.toString());
params.set("page", String(nextPage));
params.set("limit", String(limit));
return `${pathname}?${params.toString()}`;
};
return (
<nav aria-label="Pagination">
<Link
href={buildHref(page - 1)}
aria-disabled={page <= 1}
className={page <= 1 ? "disabled" : ""}
>
السابق
</Link>
<span>
صفحة {page} من {totalPages}
</span>
<Link
href={buildHref(page + 1)}
aria-disabled={page >= totalPages}
className={page >= totalPages ? "disabled" : ""}
>
التالي
</Link>
</nav>
);
}هذا النمط يبقي جلب البيانات على الخادم مع السماح للمتصفح بالتعامل مع التنقل. عنوان URL قابل للمشاركة والتحديث والتاريخ. وبما يحدث الجلب على الخادم، فإن بيانات الصفحة المخزنة مؤقتًا تستفيد من نفس معاني تخزين fetch مؤقتًا مثل أي طلب آخر للمكون الخادمي.
الجزء الأكثر صعوبة هو التأكد من أن مفتاح التخزين المؤقت يعكس معلمات الاستعلام. يتضمن Next.js searchParams تلقائيًا في مفتاح التخزين المؤقت لمقاطع المسار. ولكن إذا قمت بلف جلب البيانات في unstable_cache أو دالة تخزين مؤقت مخصصة، فيجب عليك تضمين المعلمات في المفتاح بنفسك.
import { unstable_cache } from "next/cache";
const getPostsCached = unstable_cache(
async (page: number, limit: number) => getPosts({ page, limit }),
["posts-list"],
{ revalidate: 60, tags: ["posts"] },
);لاحظ أن unstable_cache تستخدم وسائط الدالة لبناء المفتاح، لذا فإن page وlimit هما بالفعل جزء من هوية التخزين المؤقت. إذا قمت ببناء ذاكرة التخزين المؤقت الخاصة بك، فتأكد من فعل الشيء نفسه. نسيان تضمين معلمات ترقيم الصفحات في مفتاح التخزين المؤقت هو أسرع طريقة لعرض الصفحة الأولى في كل صفحة.
التعامل مع التحويرات وإعادة التحقق
جلب البيانات على الخادم هو نصف القصة فقط. تحتاج أيضًا إلى تحديث البيانات. في App Router، تتم معالجة التحويرات باستخدام إجراءات الخادم. إجراء الخادم هو دالة غير متزامنة تعمل على الخادم ويمكن استدعاؤها من مكون العميل. بعد اكتمال الإجراء، يمكنك إعادة التحقق من صحة ذاكرة التخزين المؤقت حتى يرى العرض التالي بيانات جديدة.
"use server";
import { revalidatePath } from "next/cache";
export async function publishComment(formData: FormData) {
const rawFormData = {
postId: formData.get("postId") as string,
body: formData.get("body") as string,
};
await db.insert("comments", rawFormData);
revalidatePath(`/blog/${rawFormData.postId}`);
}// CommentForm.tsx
"use client";
import { publishComment } from "./actions";
export function CommentForm({ postId }: { postId: string }) {
return (
<form action={publishComment}>
<input type="hidden" name="postId" value={postId} />
<textarea name="body" required />
<button type="submit">نشر التعليق</button>
</form>
);
}هذا النمط يبقي منطق التحوير على الخادم مع بقاء واجهة المستخدم تفاعلية. إبطال ذاكرة التخزين المؤقت صريح. تقرر أي المسارات أصبحت قديمة وتحتاج إلى إعادة العرض. هذه ميزة، لا خطأ. الإبطال الضمني هو مصدر البيانات الوهمية التي لا يمكن لأحد تفسيرها.
استراتيجية الهجرة التي تعمل فعليًا
الانتقال من جلب useEffect إلى المكونات الخادمية ليس إعادة كتابة شاملة. هاجرنا صفحة بصفحة، بدءًا من المسارات الأكثر ثباتًا والأكثر زيارة. كان النمط هو نفسه دائمًا.
- تحديد صفحة تجلب البيانات في
useEffect. - نقل الجلب إلى مكون خادمي غير متزامن.
- إضافة
loading.tsxوerror.tsxأو تحديثهما لهذا القطاع. - نقل التفاعلية الخاصة بالعميل إلى مكونات عميل أصغر باستخدام
"use client". - إضافة حدود
Suspenseحول البيانات البطيئة أو غير الحرجة. - ضبط التخزين المؤقت باستخدام
revalidateأوcache: "no-store"أو العرض الديناميكي حسب الحاجة.
الجزء الأصعب كان التخلص من رد الفعل تجاه إدارة الحالة. كان المطورون يلجأون إلى useState من العادة، حتى عندما لم يكن هناك حالة لإدارتها. أصبحت مراجعة الكود المكان الذي نسأل فيه: "هل تحتاج هذه البيانات إلى المتصفح؟" إذا كانت الإجابة لا، انتقلت إلى الخادم.
المزالق الشائعة
المزالق الأولى هي التعامل مع المكونات الخادمية كبديل لمسارات API. ليست كذلك. تقدم المكونات الخادمية العرض مرة واحدة لكل طلب ويجب ألا تنفذ آثارًا جانبية مثل إرسال رسائل البريد الإلكتروني أو الكتابة إلى السجلات بطريقة تعتمد على ترتيب العرض. للإجراءات والآثار الجانبية، استخدم إجراءات الخادم أو مسارات API.
المزالق الثاني هو التخزين المؤقت المفرط. سلوك التخزين المؤقت الافتراضي عدواني، وهو أمر رائع للأداء وخطير للبيانات التي تتغير بشكل متكرر. تعلمنا أن نكون صريحين بشأن كل عملية جلب. إذا كان يجب ألا يتم تخزين الطلب مؤقتًا، فقل ذلك.
const data = await fetch("https://api.example.com/stats", {
cache: "no-store",
});المزالق الثالث هو نقل الكثير إلى العميل. في كل مرة أضفنا فيها "use client" إلى مكون كان في الغالب توضيحيًا، فقدنا فوائد العرض من جانب الخادم لهذه الشجرة الفرعية. نحافظ الآن على مكونات العميل صغيرة وورقية قدر الإمكان.
ما قمنا بقياسه
بعد ترحيل صفحاتنا الأكثر زيارة، رأينا ثلاث تحسينات. انخفض Time to First Byte لأن الخادم يمكنه جلب البيانات بالتوازي مع العرض بدلاً من انتظار ترطيب المتصفح أولاً. تحسن Largest Contentful Paint لأن المحتوى وصل كـ HTML، وليس كحمولة JavaScript لا تزال بحاجة إلى التنفيذ. وانكمش حزمة JavaScript لأن كود جلب البيانات توقف عن الشحن إلى العميل.
كان التحسين النوعي مهماً بنفس القدر. أصبحت الصفحات أسهل في الاختبار لأن تبعيات البيانات كانت وسائط دالة صريحة بدلاً من أن تكون مخبأة داخل سلاسل الخطافات. أصبحت المكونات أسهل في إعادة الاستخدام لأنها لم تعد بحاجة إلى معرفة مصدر بياناتها. وأصبح تصحيح الأخطاء أسهل لأن هناك عددًا أقل من حالات السباق وأخطاء الإلغاء للمطاردة.
النتائج المستخلصة
useEffectهو مدير للآثار الجانبية، وليس طبقة جلب بيانات. التعامل معه على هذا النحو يخلق مشاكل في التحميل والخطأ والتخزين المؤقت والإلغاء في كل مكان.- اجلب البيانات على الخادم افتراضيًا، خاصة لعرض الصفحة الأولية. يمكن أن تكون المكونات الخادمية غير متزامنة وتستفيد من طبقات التخزين المؤقت في Next.js.
- استخدم
loading.tsxوerror.tsxوحدودSuspenseللعرض التدريجي. لا تبني حالات تحميل في كل مكون. - احتفظ بجلب جانب العميل للتفاعلات والتحديثات في الوقت الفعلي والبيانات المتاحة فقط في المتصفح. حافظ على مكونات العميل صغيرة وورقية.
- استخدم إجراءات الخادم للتحويرات وأبطل ذاكرة التخزين المؤقت صراحةً. الإبطال الضمني هو مصدر البيانات الوهمية.
- الهجرة تتم تدريجيًا. ابدأ بالصفحات الثابتة ذات الحركة المرورية العالية وانتقل للأسفل.
- كن صريحًا بشأن التخزين المؤقت. الإعدادات الافتراضية العدوانية رائعة حتى تخفي عنك البيانات القديمة.

كتبه
Miracle Kalu
Senior Full Stack Engineer
نُشر 13 يونيو 2026 · 8 دقيقة قراءة