تخطَّ إلى المحتوى

٩ يوليو ٢٠٢٦

حل حلقة useEffect اللانهائية في React بالعربي

شارك المقال:

تفتح تبويب الشبكة فتجد مئات الطلبات لنفس الـ API، أو يظهر:

Warning: Maximum update depth exceeded. This can happen when a
component calls setState inside useEffect...

هذه أشهر ورطة في React بلا منازع. سببها دائمًا معادلة واحدة: effect يغيّر حالة → الحالة تعيد الرندر → الرندر يعيد تشغيل الـ effect. لنكسر الحلقة من مصدرها.

الأسباب الثلاثة

السببالشكلالحل
لا مصفوفة تبعياتuseEffect(() => {...})أضف [] أو التبعيات الصحيحة
تبعية تُنشأ كل رندركائن/مصفوفة/دالة في المصفوفةuseMemo / useCallback
يحدّث تبعيته بنفسهsetCount(count + 1) و[count]تحديث دالّي بلا تبعية

1) نسيت مصفوفة التبعيات

بدون المصفوفة، الـ effect يعمل بعد كل رندر — وبما أنه يعمل setState، كل تشغيلة تلد رندرًا يلد تشغيلة:

// ❌ حلقة فورية
useEffect(() => {
  fetch("/api/lessons")
    .then((r) => r.json())
    .then(setLessons);
});

// ✅ مرة واحدة بعد أول تركيب
useEffect(() => {
  fetch("/api/lessons")
    .then((r) => r.json())
    .then(setLessons);
}, []);

2) تبعية بمرجع جديد في كل رندر

المقارنة تتم بالمرجع لا بالمحتوى — وكل رندر ينشئ الكائن من جديد:

// ❌ options كائن جديد كل رندر → الـ effect «يرى تغييرًا» كل مرة
function Search({ query }) {
  const options = { lang: "ar", query };
  useEffect(() => {
    runSearch(options).then(setResults);
  }, [options]);
}

ثلاث طرق للتثبيت، اختر الأبسط الممكن:

// ✅ أ: اعتمد على القيم البدائية نفسها — أبسط حل وأفضله
useEffect(() => {
  runSearch({ lang: "ar", query }).then(setResults);
}, [query]);

// ✅ ب: ثبّت الكائن بـ useMemo إذا كان يُستخدم في أماكن أخرى
const options = useMemo(() => ({ lang: "ar", query }), [query]);

// ✅ ج: الدوال تُثبّت بـ useCallback
const load = useCallback(() => runSearch({ query }), [query]);

💡 والثابت الذي لا يعتمد على props؟ انقله خارج المكوّن نهائيًّا — لا useMemo ولا تبعيات أصلًا.

3) الـ effect يحدّث التبعية التي يراقبها

// ❌ يقرأ count ويكتب count → حلقة مثالية
useEffect(() => {
  const id = setInterval(() => setCount(count + 1), 1000);
  return () => clearInterval(id);
}, [count]);

// ✅ التحديث الدالّي لا يحتاج قراءة count → التبعية تسقط
useEffect(() => {
  const id = setInterval(() => setCount((c) => c + 1), 1000);
  return () => clearInterval(id);
}, []);

setCount(c => c + 1) تستلم أحدث قيمة من React نفسه — بلا الاعتماد على متغير الرندر الحالي.

كيف تشخّص أي سبب عندك؟

  1. أضف console.log("effect ran") داخل الـ effect — هل يطبع بلا توقف؟
  2. اطبع التبعيات نفسها قبل الـ effect — أيّها يتغير كل رندر؟ (الكائنات والدوال هي المشتبه الأول.)
  3. ثق بتحذير react-hooks/exhaustive-deps — لا تسكته، أصلح سببه.

قائمة تحقّق سريعة

  • هل لكل useEffect مصفوفة تبعيات مقصودة؟
  • هل بين التبعيات كائن أو مصفوفة أو دالة تُنشأ داخل المكوّن؟ ثبّتها أو استبدلها بقيمها البدائية.
  • هل يحدّث الـ effect حالةً هي نفسها في تبعياته؟ → تحديث دالّي.
  • هل تنظّف المؤقتات والاشتراكات في دالة الإرجاع؟

الخلاصة

حلقة useEffect اللانهائية ليست لغزًا: ابحث عن «من يعيد تشغيلي؟» — إمّا مصفوفة غائبة، أو مرجع يتجدد كل رندر، أو effect يعضّ ذيله بتحديث تبعيته. القاعدة الذهبية: تبعيات صادقة + مراجع مستقرة + تحديثات دالّية.

رسّخ الأساس في درس الحالة State من مسار React بالعربي، واقرأ حل خطأ Hydration في React وNext.js و جدول أخطاء TypeError في جافاسكريبت.

📚 مصادر رسمية للتعمّق: التوثيق الرسمي لـ React

الأسئلة الشائعة

ما علاقة Maximum update depth exceeded بالحلقة اللانهائية؟

هي نفس المشكلة وقد انفجرت: React اكتشف سلسلة تحديثات حالة لا تتوقف (رندر يشغّل effect يحدّث حالة تعيد الرندر...) فأوقفها بهذا الخطأ حمايةً للمتصفح.

لماذا يعتبر React الكائن الجديد «تغيّر» رغم أن محتواه نفسه؟

لأن مقارنة التبعيات تتم بالمرجع (===) لا بالمحتوى. كل رندر ينشئ {} أو [] أو دالة جديدة بمرجع جديد، فتبدو التبعية متغيرة في كل مرة ويعاد تشغيل الـ effect.

هل الحل أن أحذف مصفوفة التبعيات أو أتركها فارغة دائمًا؟

لا — حذفها يشغّل الـ effect بعد كل رندر، وتفريغها زورًا يخفي تحذير ESLint ويجلب بيانات قديمة (stale). الصحيح: اذكر كل ما يستخدمه الـ effect فعلًا، وثبّت المراجع غير المستقرة بـ useMemo/useCallback أو انقلها خارج المكوّن.

اقرأ أيضًا

تصفّح كل المقالات في المدوّنة، أو ابدأ التعلّم من المسارات و خرائط الطريق.