تقوم بتحديث الصفحة وتظل الشاشة فارغة. أو ربما يؤدي النقر على زر ما إلى جعل مروحة المعالج (CPU fan) تعمل بأقصى طاقتها. ثم تخرج وحدة التحكم (console) التحذير المرعب: Maximum update depth exceeded. لقد ضغط React على المكابح لأن مكونك عالق في حلقة مفرغة (infinite loop). إنه أحد أكثر الأخطاء شيوعاً في تطبيقات React، وعادة ما ينبع من سوء فهم بسيط حول متى يتم تنفيذ الكود الخاص بك بالفعل.
لإصلاحه، تحتاج إلى فهم اللحظة الدقيقة التي يتحول فيها الـ render إلى re-render، ولماذا يجب أن تظل تغييرات الحالة (state changes) بعيدة عن مسار الـ render.
كيف يقوم React بعملية الـ Render لمكونك
يقوم React ببناء واجهات المستخدم من مكونات (components). في React الحديث، تكون هذه المكونات عبارة عن دوال (functions). في كل مرة يحتاج فيها React إلى عرض مكونك على الشاشة، فإنه ببساطة يستدعي تلك الدالة. داخل الدالة، يمكنك استخدام state لتذكر الأشياء بين عمليات الـ render. تخبر الـ state نظام React بالبيانات التي تنتمي للمكون، والأهم من ذلك، متى تغير شيء ما وحاجت واجهة المستخدم (UI) إلى التحديث.
عندما تتغير الـ state، يقوم React بجدولة render جديد. تعمل دالة المكون مرة أخرى، وتُرجع JSX جديداً، ويقوم React بتحديث الـ DOM ليتطابق معه. في الاستخدام الطبيعي، تكون هذه الدورة غير ضارة. تنقر على زر، يقوم معالج الحدث (event handler) بتحديث الـ state، يقوم React بعمل re-render مرة واحدة، ويرى المستخدم النص أو اللون الجديد.
يظهر الخطأ عندما يتسبب الـ render نفسه في تحديث حالة آخر. يؤدي تحديث الحالة الجديد هذا إلى trigger لـ render آخر، والذي بدوره يؤدي إلى trigger لتحديث حالة آخر. يتحمل React هذا لعدة عشرات من الدورات، ثم يلقي بخطأ الحد الأقصى للعمق (maximum depth error) لحماية المتصفح من التجمد تماماً.
نظرة سريعة على الـ State
قبل تشريح الحلقة، تذكر كيف يعمل hook الـ useState. فهو يعطيك شيئين بالضبط: متغير يحمل القيمة الحالية، ودالة لتغيير تلك القيمة.
const MessageComponent = () => {
const [message, setMessage] = useState('Welcome');
return <h1>{message}</h1>;
};
هنا، تكون message هي 'Welcome' في أول render. إذا قمت لاحقاً باستدعاء setMessage('Goodbye') ، فإن React يسجل التغيير، ويستدعي MessageComponent مرة أخرى، وتظهر الآن "Goodbye" في واجهة المستخدم. كل شيء يسير على ما يرام لأن لا شيء في جسم المكون نفسه يقوم باستدعاء دالة التعيين (setter) تلقائياً. تبدأ الحلقة عندما يتم تشغيل الـ setter أثناء مرحلة الـ render دون وجود حدث خارجي.
استدعاء SetState مباشرة داخل جسم المكون
الطريقة الأكثر مباشرة لإنشاء حلقة مفرغة هي استدعاء دالة تعيين الحالة (state setter function) مباشرة داخل جسم المكون. وبما أن جسم المكون يتم تنفيذه عند كل render، فإن الـ setter يتم تشغيله عند كل render. يؤدي تحديث الحالة الجديد هذا إلى render آخر. وتستمر الدورة إلى الأبد.
إليك كيف يبدو الخطأ:
const Counter = () => {
const [count, setCount] = useState(0);
setCount(count + 1);
return <div>{count}</div>;
};
في كل مرة يتم فيها عمل render لـ Counter ، فإنه يزيد count. يقوم React بعمل render مرة أخرى لعرض الرقم الجديد، ويرى setCount(count + 1) مرة أخرى، ويزيد مرة أخرى. الحل مباشر: لا تستدعي أبداً دالة تعيين الحالة (state setter) في المستوى الأعلى (top level) لمكونك أثناء الـ render. يجب أن تستجيب تحديثات الحالة لأحداث المستخدم أو الآثار الجانبية (side effects)، وليس لعملية رسم الشاشة. انقل ذلك التحديث إلى معالج حدث (event handler) بدلاً من ذلك:
const Counter = () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
};
الاستثناء الوحيد لاستدعاء الـ setters أثناء الـ render هو عندما تقوم بحساب state جديدة من الـ props، وحتى في هذه الحالة يجب عليك استخدام نمط مختلف، مثل اشتقاق القيمة مباشرة أو استخدام useEffect بشكل متعمد.
تمرير استدعاء دالة بدلاً من مرجع لها
سبب متكرر آخر هو خطأ مطبعي بسيط في JSX. عند إرفاق معالج حدث (event handler)، تحتاج إلى تمرير الدالة نفسها. إذا قمت عن طريق الخطأ باستدعاء الدالة هناك مباشرة في JSX، فستعمل فوراً أثناء دورة الـ render.
const Toggle = () => {
const [isOn, setIsOn] = useState(false);
const handleToggle = () => setIsOn(!isOn);
return <button onClick={handleToggle()}>Toggle</button>;
};
بكتابة handleToggle() مع الأقواس، أنت لا تعطي React دالة ليستدعيها لاحقاً عندما ينقر المستخدم. أنت تستدعيها الآن بينما يقوم React ببناء الـ virtual DOM. وبما أن handleToggle تقوم بتحديث الـ state، فإن المكون يعيد الـ render. خلال عملية الـ re-render تلك، يرى React handleToggle() مرة أخرى ويستدعيها مرة أخرى. لا تنتهي الحلقة أبداً. النسخة الصحيحة تزيل الأقواس:
return <button onClick={handleToggle}>Toggle</button>;
إذا كنت بحاجة إلى تمرير وسائط (arguments)، فقم بتغليف الاستدعاء في دالة مجهولة (anonymous function):
return <button onClick={() => handleToggle(true)}>Switch On</button>;
هذا التمييز يربك حتى المطورين ذوي الخبرة أثناء إعادة هيكلة الكود (refactoring). انتبه جيداً لتلك الأقواس.
فخ تبعيات useEffect
تعتبر الـ Effects هي المكان المناسب للآثار الجانبية (side effects) مثل جلب البيانات، أو المزامنة مع واجهات برمجة تطبيقات المتصفح (browser APIs)، أو التلاعب بالـ DOM يدوياً. لكن useEffect يعمل بعد أن يقوم React بتثبيت (commit) الـ render على الشاشة. إذا قام الـ effect الخاص بك بتحديث الـ state، فسيقوم React بعمل re-render. عادة ما يكون ذلك أمراً جيداً. لكنه يصبح حلقة مفرغة عندما يعمل الـ effect بعد كل render ويقوم دائماً بتحديث نفس الـ state.
Consider this broken pattern:
const UserProfile = () => {
const [user, setUser] = useState({});
useEffect(() => {
setUser({ name: 'Ada', role: 'Admin' });
});
return <div>{user.name}</div>;
};
Because there is no dependency array, this effect runs after every single render. It sets user, which triggers a render. After that render, the effect runs again and sets user again. React detects the spiral and throws the error.
The fix is to tell React when the effect actually needs to run by supplying a proper dependency array. If the effect should only run once on mount, pass an empty array:
useEffect(() => {
setUser({ name: 'Ada', role: 'Admin' });
}, []);
If the effect depends on a prop or a piece of state, include only that variable in the array. Be careful, though. Including a variable that changes every render will just recreate the same loop through a different door. For example, if you include an object literal in the dependencies and that object is recreated on every parent render, the effect will fire endlessly. In those cases, you may need to move the object creation outside the component or memoize it.
Practical Debugging Steps
When you hit this error, the stack trace can look overwhelming because React has already repeated the cycle dozens of times. Start by reading the top of the trace to find which component is named repeatedly. Then look for state setters in these three places:
- The main body of the component, outside any handler or hook.
- JSX event attributes where you might have written
handler()instead ofhandler. useEffecthooks that lack a dependency array or depend on unstable references.
Temporarily comment out each state setter until the error stops. That tells you exactly which update is the culprit. If the setter is inside an effect, ask yourself whether you even need state there. Sometimes developers set local state from props inside an effect when they could simply use the prop directly in the JSX.
The Real Takeaway
The Maximum update depth error is not a mysterious React bug. It is a safety net. It means your component is trying to re-render itself instead of waiting for an external signal. Break the habit of treating renders as events that should produce more state. Treat renders as pure consequences of state, not as causes. Keep state updates inside event handlers, callbacks, or effects with carefully chosen dependencies, and you will never see this error again.
