تفتح تبويب الشبكة فتجد مئات الطلبات لنفس الـ 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 نفسه — بلا الاعتماد على
متغير الرندر الحالي.
كيف تشخّص أي سبب عندك؟
- أضف
console.log("effect ran")داخل الـ effect — هل يطبع بلا توقف؟ - اطبع التبعيات نفسها قبل الـ effect — أيّها يتغير كل رندر؟ (الكائنات والدوال هي المشتبه الأول.)
- ثق بتحذير
react-hooks/exhaustive-deps— لا تسكته، أصلح سببه.
قائمة تحقّق سريعة
- هل لكل
useEffectمصفوفة تبعيات مقصودة؟ - هل بين التبعيات كائن أو مصفوفة أو دالة تُنشأ داخل المكوّن؟ ثبّتها أو استبدلها بقيمها البدائية.
- هل يحدّث الـ effect حالةً هي نفسها في تبعياته؟ → تحديث دالّي.
- هل تنظّف المؤقتات والاشتراكات في دالة الإرجاع؟
الخلاصة
حلقة useEffect اللانهائية ليست لغزًا: ابحث عن «من يعيد تشغيلي؟» — إمّا مصفوفة غائبة، أو مرجع يتجدد كل رندر، أو effect يعضّ ذيله بتحديث تبعيته. القاعدة الذهبية: تبعيات صادقة + مراجع مستقرة + تحديثات دالّية.
رسّخ الأساس في درس الحالة State من مسار React بالعربي، واقرأ حل خطأ Hydration في React وNext.js و جدول أخطاء TypeError في جافاسكريبت.